From c790b698f20a021e6c6ce36c9dfabf1dfb234513 Mon Sep 17 00:00:00 2001 From: appflowy Date: Thu, 22 Sep 2022 21:02:10 +0800 Subject: [PATCH 01/25] fix: open card error --- frontend/app_flowy/lib/plugins/board/presentation/card/card.dart | 1 + 1 file changed, 1 insertion(+) 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 3430cacec6..6e555b87c7 100644 --- a/frontend/app_flowy/lib/plugins/board/presentation/card/card.dart +++ b/frontend/app_flowy/lib/plugins/board/presentation/card/card.dart @@ -83,6 +83,7 @@ class _BoardCardState extends State { builder: (context, state) { return AppFlowyPopover( controller: popoverController, + triggerActions: PopoverTriggerFlags.none, constraints: BoxConstraints.loose(const Size(140, 200)), direction: PopoverDirection.rightWithCenterAligned, popupBuilder: (popoverContext) => _handlePopoverBuilder( From 5464f1851beb42052670429d2d873e5534d54271 Mon Sep 17 00:00:00 2001 From: appflowy Date: Thu, 22 Sep 2022 21:12:41 +0800 Subject: [PATCH 02/25] fix: UI layout issue when date select panel popup --- .../widgets/cell/date_cell/date_cell.dart | 22 ++------ .../widgets/cell/date_cell/date_editor.dart | 55 ++++++++++--------- .../flowy_infra_ui/lib/flowy_infra_ui.dart | 2 +- ...ype_popover.dart => appflowy_popover.dart} | 44 +++++++++++++-- .../lib/src/flowy_overlay/flowy_overlay.dart | 1 - .../lib/src/flowy_overlay/list_overlay.dart | 35 +++++++++++- .../src/flowy_overlay/overlay_container.dart | 34 ------------ 7 files changed, 111 insertions(+), 82 deletions(-) rename frontend/app_flowy/packages/flowy_infra_ui/lib/src/flowy_overlay/{appflowy_stype_popover.dart => appflowy_popover.dart} (55%) delete mode 100644 frontend/app_flowy/packages/flowy_infra_ui/lib/src/flowy_overlay/overlay_container.dart 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 2a7dd1de52..d55ad8ada0 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 @@ -65,23 +65,17 @@ class _DateCellState extends GridCellState { builder: (context, state) { return AppFlowyPopover( controller: _popover, - offset: const Offset(0, 20), + triggerActions: PopoverTriggerFlags.none, direction: PopoverDirection.bottomWithLeftAligned, constraints: BoxConstraints.loose(const Size(320, 500)), + margin: EdgeInsets.zero, child: SizedBox.expand( child: GestureDetector( behavior: HitTestBehavior.opaque, - onTap: () => _showCalendar(context), - child: MouseRegion( - opaque: false, - cursor: SystemMouseCursors.click, - child: Align( - alignment: alignment, - child: FlowyText.medium( - state.dateStr, - fontSize: 12, - ), - ), + onTap: () => _popover.show(), + child: Align( + alignment: alignment, + child: FlowyText.medium(state.dateStr, fontSize: 12), ), ), ), @@ -101,10 +95,6 @@ class _DateCellState extends GridCellState { ); } - void _showCalendar(BuildContext context) { - _popover.show(); - } - @override Future dispose() async { _cellBloc.close(); 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 7678d261c2..f478c8cc2b 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 @@ -2,6 +2,7 @@ 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:appflowy_popover/appflowy_popover.dart'; +import 'package:dartz/dartz.dart' show Either; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra/image.dart'; import 'package:flowy_infra/theme.dart'; @@ -11,6 +12,7 @@ import 'package:flowy_infra_ui/style_widget/text.dart'; import 'package:flowy_infra_ui/widget/rounded_input_field.dart'; import 'package:flowy_infra_ui/widget/spacing.dart'; import 'package:flowy_sdk/log.dart'; +import 'package:flowy_sdk/protobuf/flowy-error/errors.pbserver.dart'; import 'package:flowy_sdk/protobuf/flowy-grid/date_type_option.pb.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -39,34 +41,37 @@ class DateCellEditor extends StatefulWidget { } class _DateCellEditor extends State { - DateTypeOptionPB? _dateTypeOptionPB; - - @override - void initState() { - super.initState(); - _fetchData(); - } - - _fetchData() async { - final result = await widget.cellController - .getFieldTypeOption(DateTypeOptionDataParser()); - - result.fold((dateTypeOptionPB) { - setState(() { - _dateTypeOptionPB = dateTypeOptionPB; - }); - }, (err) => Log.error(err)); - } - @override Widget build(BuildContext context) { - if (_dateTypeOptionPB == null) { - return Container(); - } + return FutureBuilder>( + future: widget.cellController.getFieldTypeOption( + DateTypeOptionDataParser(), + ), + builder: (BuildContext context, snapshot) { + if (snapshot.hasData) { + return _buildWidget(snapshot); + } else { + return const SizedBox(); + } + }, + ); + } - return _CellCalendarWidget( - cellContext: widget.cellController, - dateTypeOptionPB: _dateTypeOptionPB!, + Widget _buildWidget(AsyncSnapshot> snapshot) { + return snapshot.data!.fold( + (dateTypeOptionPB) { + return Padding( + padding: const EdgeInsets.all(12), + child: _CellCalendarWidget( + cellContext: widget.cellController, + dateTypeOptionPB: dateTypeOptionPB, + ), + ); + }, + (err) { + Log.error(err); + return const SizedBox(); + }, ); } } diff --git a/frontend/app_flowy/packages/flowy_infra_ui/lib/flowy_infra_ui.dart b/frontend/app_flowy/packages/flowy_infra_ui/lib/flowy_infra_ui.dart index adc7dd1f9b..fa20e57294 100644 --- a/frontend/app_flowy/packages/flowy_infra_ui/lib/flowy_infra_ui.dart +++ b/frontend/app_flowy/packages/flowy_infra_ui/lib/flowy_infra_ui.dart @@ -9,4 +9,4 @@ export 'src/flowy_overlay/flowy_overlay.dart'; export 'src/flowy_overlay/list_overlay.dart'; export 'src/flowy_overlay/option_overlay.dart'; export 'src/flowy_overlay/flowy_dialog.dart'; -export 'src/flowy_overlay/appflowy_stype_popover.dart'; +export 'src/flowy_overlay/appflowy_popover.dart'; diff --git a/frontend/app_flowy/packages/flowy_infra_ui/lib/src/flowy_overlay/appflowy_stype_popover.dart b/frontend/app_flowy/packages/flowy_infra_ui/lib/src/flowy_overlay/appflowy_popover.dart similarity index 55% rename from frontend/app_flowy/packages/flowy_infra_ui/lib/src/flowy_overlay/appflowy_stype_popover.dart rename to frontend/app_flowy/packages/flowy_infra_ui/lib/src/flowy_overlay/appflowy_popover.dart index e4c3ba6369..7885cbb01d 100644 --- a/frontend/app_flowy/packages/flowy_infra_ui/lib/src/flowy_overlay/appflowy_stype_popover.dart +++ b/frontend/app_flowy/packages/flowy_infra_ui/lib/src/flowy_overlay/appflowy_popover.dart @@ -1,7 +1,10 @@ -import 'package:flowy_infra_ui/flowy_infra_ui_web.dart'; import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:flutter/material.dart'; +import 'package:flowy_infra/theme.dart'; +import 'package:flowy_infra_ui/style_widget/decoration.dart'; +import 'package:provider/provider.dart'; + class AppFlowyPopover extends StatelessWidget { final Widget child; final PopoverController? controller; @@ -13,6 +16,7 @@ class AppFlowyPopover extends StatelessWidget { final PopoverMutex? mutex; final Offset? offset; final bool asBarrier; + final EdgeInsets margin; const AppFlowyPopover({ Key? key, @@ -26,6 +30,7 @@ class AppFlowyPopover extends StatelessWidget { this.offset, this.controller, this.asBarrier = false, + this.margin = const EdgeInsets.all(12), }) : super(key: key); @override @@ -39,13 +44,44 @@ class AppFlowyPopover extends StatelessWidget { triggerActions: triggerActions, popupBuilder: (context) { final child = popupBuilder(context); - debugPrint('$child popover'); - return OverlayContainer( + debugPrint('Show $child popover'); + return _PopoverContainer( constraints: constraints, - child: popupBuilder(context), + margin: margin, + child: child, ); }, child: child, ); } } + +class _PopoverContainer extends StatelessWidget { + final Widget child; + final BoxConstraints? constraints; + final EdgeInsets margin; + const _PopoverContainer({ + required this.child, + required this.margin, + this.constraints, + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + final theme = context.watch(); + final decoration = FlowyDecoration.decoration( + theme.surface, + theme.shadowColor.withOpacity(0.15), + ); + return Material( + type: MaterialType.transparency, + child: Container( + padding: margin, + decoration: decoration, + constraints: constraints, + child: child, + ), + ); + } +} diff --git a/frontend/app_flowy/packages/flowy_infra_ui/lib/src/flowy_overlay/flowy_overlay.dart b/frontend/app_flowy/packages/flowy_infra_ui/lib/src/flowy_overlay/flowy_overlay.dart index 15797453e6..fc18de2a29 100644 --- a/frontend/app_flowy/packages/flowy_infra_ui/lib/src/flowy_overlay/flowy_overlay.dart +++ b/frontend/app_flowy/packages/flowy_infra_ui/lib/src/flowy_overlay/flowy_overlay.dart @@ -4,7 +4,6 @@ import 'dart:ui'; import 'package:flowy_infra_ui/src/flowy_overlay/layout.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -export './overlay_container.dart'; /// Specifies how overlay are anchored to the SourceWidget enum AnchorDirection { diff --git a/frontend/app_flowy/packages/flowy_infra_ui/lib/src/flowy_overlay/list_overlay.dart b/frontend/app_flowy/packages/flowy_infra_ui/lib/src/flowy_overlay/list_overlay.dart index 0b247267eb..24483a149f 100644 --- a/frontend/app_flowy/packages/flowy_infra_ui/lib/src/flowy_overlay/list_overlay.dart +++ b/frontend/app_flowy/packages/flowy_infra_ui/lib/src/flowy_overlay/list_overlay.dart @@ -1,5 +1,8 @@ -import 'package:flowy_infra_ui/flowy_infra_ui_web.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; +import 'package:flowy_infra/theme.dart'; +import 'package:flowy_infra_ui/style_widget/decoration.dart'; +import 'package:provider/provider.dart'; class ListOverlayFooter { Widget widget; @@ -96,3 +99,33 @@ class ListOverlay extends StatelessWidget { ); } } + +const overlayContainerPadding = EdgeInsets.all(12); + +class OverlayContainer extends StatelessWidget { + final Widget child; + final BoxConstraints? constraints; + final EdgeInsets padding; + const OverlayContainer({ + required this.child, + this.constraints, + this.padding = overlayContainerPadding, + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + final theme = + context.watch() ?? AppTheme.fromType(ThemeType.light); + return Material( + type: MaterialType.transparency, + child: Container( + padding: padding, + decoration: FlowyDecoration.decoration( + theme.surface, theme.shadowColor.withOpacity(0.15)), + constraints: constraints, + child: child, + ), + ); + } +} diff --git a/frontend/app_flowy/packages/flowy_infra_ui/lib/src/flowy_overlay/overlay_container.dart b/frontend/app_flowy/packages/flowy_infra_ui/lib/src/flowy_overlay/overlay_container.dart deleted file mode 100644 index 91206253b1..0000000000 --- a/frontend/app_flowy/packages/flowy_infra_ui/lib/src/flowy_overlay/overlay_container.dart +++ /dev/null @@ -1,34 +0,0 @@ -import 'package:flowy_infra/theme.dart'; -import 'package:flowy_infra_ui/style_widget/decoration.dart'; -import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; - -const overlayContainerPadding = EdgeInsets.all(12); - -class OverlayContainer extends StatelessWidget { - final Widget child; - final BoxConstraints? constraints; - final EdgeInsets padding; - const OverlayContainer({ - required this.child, - this.constraints, - this.padding = overlayContainerPadding, - Key? key, - }) : super(key: key); - - @override - Widget build(BuildContext context) { - final theme = - context.watch() ?? AppTheme.fromType(ThemeType.light); - return Material( - type: MaterialType.transparency, - child: Container( - padding: padding, - decoration: FlowyDecoration.decoration( - theme.surface, theme.shadowColor.withOpacity(0.15)), - constraints: constraints, - child: child, - ), - ); - } -} From fc1535696b99c4eb5f8462a2f217ee1a125c6fcb Mon Sep 17 00:00:00 2001 From: Richard Shiue <71320345+richardshiue@users.noreply.github.com> Date: Fri, 23 Sep 2022 01:20:01 +0800 Subject: [PATCH 03/25] fix: color code for tint 9 --- frontend/app_flowy/packages/flowy_infra/lib/theme.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/app_flowy/packages/flowy_infra/lib/theme.dart b/frontend/app_flowy/packages/flowy_infra/lib/theme.dart index 04b4a19fdb..9bb52c89e7 100644 --- a/frontend/app_flowy/packages/flowy_infra/lib/theme.dart +++ b/frontend/app_flowy/packages/flowy_infra/lib/theme.dart @@ -104,7 +104,7 @@ class AppTheme { ..tint6 = const Color(0xfff5ffdc) ..tint7 = const Color(0xffddffd6) ..tint8 = const Color(0xffdefff1) - ..tint9 = const Color(0xffdefff1) + ..tint9 = const Color(0xffe1fbff) ..main1 = const Color(0xff00bcf0) ..main2 = const Color(0xff00b7ea) ..textColor = _black From e5f5169a399751eb79655cb31155c31e05de4c84 Mon Sep 17 00:00:00 2001 From: Richard Shiue <71320345+richardshiue@users.noreply.github.com> Date: Fri, 23 Sep 2022 01:21:04 +0800 Subject: [PATCH 04/25] style: code auto-format --- .../packages/flowy_infra/lib/theme.dart | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/frontend/app_flowy/packages/flowy_infra/lib/theme.dart b/frontend/app_flowy/packages/flowy_infra/lib/theme.dart index 9bb52c89e7..cba65aa7fc 100644 --- a/frontend/app_flowy/packages/flowy_infra/lib/theme.dart +++ b/frontend/app_flowy/packages/flowy_infra/lib/theme.dart @@ -152,7 +152,8 @@ class AppTheme { ThemeData get themeData { var t = ThemeData( textTheme: TextTheme(bodyText2: TextStyle(color: textColor)), - textSelectionTheme: TextSelectionThemeData(cursorColor: main2, selectionHandleColor: main2), + textSelectionTheme: TextSelectionThemeData( + cursorColor: main2, selectionHandleColor: main2), primaryIconTheme: IconThemeData(color: hover), iconTheme: IconThemeData(color: shader1), canvasColor: shader6, @@ -179,7 +180,8 @@ class AppTheme { toggleableActiveColor: main1); } - Color shift(Color c, double d) => ColorUtils.shiftHsl(c, d * (isDark ? -1 : 1)); + Color shift(Color c, double d) => + ColorUtils.shiftHsl(c, d * (isDark ? -1 : 1)); } class ColorUtils { @@ -188,14 +190,18 @@ class ColorUtils { return hslc.withLightness((hslc.lightness + amt).clamp(0.0, 1.0)).toColor(); } - static Color parseHex(String value) => Color(int.parse(value.substring(1, 7), radix: 16) + 0xFF000000); + static Color parseHex(String value) => + Color(int.parse(value.substring(1, 7), radix: 16) + 0xFF000000); static Color blend(Color dst, Color src, double opacity) { return Color.fromARGB( 255, - (dst.red.toDouble() * (1.0 - opacity) + src.red.toDouble() * opacity).toInt(), - (dst.green.toDouble() * (1.0 - opacity) + src.green.toDouble() * opacity).toInt(), - (dst.blue.toDouble() * (1.0 - opacity) + src.blue.toDouble() * opacity).toInt(), + (dst.red.toDouble() * (1.0 - opacity) + src.red.toDouble() * opacity) + .toInt(), + (dst.green.toDouble() * (1.0 - opacity) + src.green.toDouble() * opacity) + .toInt(), + (dst.blue.toDouble() * (1.0 - opacity) + src.blue.toDouble() * opacity) + .toInt(), ); } } From 44ad0a262367d69daaee0d1ce1180aac8cfbdcb3 Mon Sep 17 00:00:00 2001 From: appflowy Date: Fri, 23 Sep 2022 11:23:35 +0800 Subject: [PATCH 05/25] fix: create the default group for grid --- .../cell/cell_service/cell_controller.dart | 7 ++- frontend/rust-lib/flowy-folder/src/manager.rs | 12 ++++- .../src/services/view/controller.rs | 21 +++++++-- frontend/rust-lib/flowy-grid/src/manager.rs | 4 +- .../src/services/grid_view_editor.rs | 16 ++++--- .../src/services/group/group_util.rs | 22 ++++----- .../flowy-sdk/src/deps_resolve/folder_deps.rs | 45 ++++++++++++++++--- .../src/revision/grid_view.rs | 4 +- .../src/client_grid/grid_revision_pad.rs | 10 +++-- .../src/client_grid/view_revision_pad.rs | 16 +++++-- shared-lib/flowy-sync/src/errors.rs | 16 ++++--- 11 files changed, 125 insertions(+), 48 deletions(-) diff --git a/frontend/app_flowy/lib/plugins/grid/application/cell/cell_service/cell_controller.dart b/frontend/app_flowy/lib/plugins/grid/application/cell/cell_service/cell_controller.dart index 29fb521976..80bd4dcb13 100644 --- a/frontend/app_flowy/lib/plugins/grid/application/cell/cell_service/cell_controller.dart +++ b/frontend/app_flowy/lib/plugins/grid/application/cell/cell_service/cell_controller.dart @@ -283,7 +283,12 @@ class IGridCellController extends Equatable { _loadDataOperation?.cancel(); _loadDataOperation = Timer(const Duration(milliseconds: 10), () { _cellDataLoader.loadData().then((data) { - _cellsCache.insert(_cacheKey, GridCell(object: data)); + if (data != null) { + _cellsCache.insert(_cacheKey, GridCell(object: data)); + } else { + _cellsCache.remove(_cacheKey); + } + _cellDataNotifier?.value = data; }); }); diff --git a/frontend/rust-lib/flowy-folder/src/manager.rs b/frontend/rust-lib/flowy-folder/src/manager.rs index 25e3b1f15b..a8ed3d56c1 100644 --- a/frontend/rust-lib/flowy-folder/src/manager.rs +++ b/frontend/rust-lib/flowy-folder/src/manager.rs @@ -221,8 +221,9 @@ impl DefaultFolderBuilder { initial_quill_delta_string() }; let _ = view_controller.set_latest_view(&view.id); + let layout_type = ViewLayoutTypePB::from(view.layout.clone()); let _ = view_controller - .create_view(&view.id, ViewDataTypePB::Text, Bytes::from(view_data)) + .create_view(&view.id, ViewDataTypePB::Text, layout_type, Bytes::from(view_data)) .await?; } } @@ -249,7 +250,13 @@ impl FolderManager { pub trait ViewDataProcessor { fn initialize(&self) -> FutureResult<(), FlowyError>; - fn create_container(&self, user_id: &str, view_id: &str, delta_data: Bytes) -> FutureResult<(), FlowyError>; + fn create_container( + &self, + user_id: &str, + view_id: &str, + layout: ViewLayoutTypePB, + delta_data: Bytes, + ) -> FutureResult<(), FlowyError>; fn close_container(&self, view_id: &str) -> FutureResult<(), FlowyError>; @@ -267,6 +274,7 @@ pub trait ViewDataProcessor { user_id: &str, view_id: &str, data: Vec, + layout: ViewLayoutTypePB, ) -> FutureResult; fn data_type(&self) -> ViewDataTypePB; 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 944a1b68db..1f0b61ebb1 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::ViewInfoPB; +use crate::entities::{ViewInfoPB, ViewLayoutTypePB}; use crate::manager::{ViewDataProcessor, ViewDataProcessorMap}; use crate::{ dart_notification::{send_dart_notification, FolderNotification}, @@ -67,10 +67,20 @@ impl ViewController { params.view_content_data = view_data.to_vec(); } else { let delta_data = processor - .create_view_from_delta_data(&user_id, ¶ms.view_id, params.view_content_data.clone()) + .create_view_from_delta_data( + &user_id, + ¶ms.view_id, + params.view_content_data.clone(), + params.layout.clone(), + ) .await?; let _ = self - .create_view(¶ms.view_id, params.data_type.clone(), delta_data) + .create_view( + ¶ms.view_id, + params.data_type.clone(), + params.layout.clone(), + delta_data, + ) .await?; }; @@ -84,6 +94,7 @@ impl ViewController { &self, view_id: &str, data_type: ViewDataTypePB, + layout_type: ViewLayoutTypePB, delta_data: Bytes, ) -> Result<(), FlowyError> { if delta_data.is_empty() { @@ -91,7 +102,9 @@ impl ViewController { } let user_id = self.user.user_id()?; let processor = self.get_data_processor(data_type)?; - let _ = processor.create_container(&user_id, view_id, delta_data).await?; + let _ = processor + .create_container(&user_id, view_id, layout_type, delta_data) + .await?; Ok(()) } diff --git a/frontend/rust-lib/flowy-grid/src/manager.rs b/frontend/rust-lib/flowy-grid/src/manager.rs index bf094b2f9d..39f8641c7e 100644 --- a/frontend/rust-lib/flowy-grid/src/manager.rs +++ b/frontend/rust-lib/flowy-grid/src/manager.rs @@ -1,3 +1,4 @@ +use crate::entities::GridLayout; use crate::services::block_editor::GridBlockRevisionCompactor; use crate::services::grid_editor::{GridRevisionCompactor, GridRevisionEditor}; use crate::services::grid_view_manager::make_grid_view_rev_manager; @@ -178,6 +179,7 @@ impl GridManager { pub async fn make_grid_view_data( user_id: &str, view_id: &str, + layout: GridLayout, grid_manager: Arc, build_context: BuildGridContext, ) -> FlowyResult { @@ -208,7 +210,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(grid_id, view_id.to_owned()); + let grid_view = GridViewRevision::new(grid_id, view_id.to_owned(), layout.into()); 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_view_editor.rs b/frontend/rust-lib/flowy-grid/src/services/grid_view_editor.rs index c7fb7f0a7f..c926743f9f 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 @@ -277,6 +277,7 @@ impl GridViewRevisionEditor { Ok(()) } + #[tracing::instrument(level = "debug", skip_all, err)] pub(crate) async fn group_by_field(&self, field_id: &str) -> FlowyResult<()> { if let Some(field_rev) = self.field_delegate.get_field_rev(field_id).await { let new_group_controller = new_group_controller_with_field_rev( @@ -374,13 +375,14 @@ impl GridViewRevisionEditor { async fn new_group_controller( user_id: String, view_id: String, - pad: Arc>, + view_rev_pad: Arc>, rev_manager: Arc, field_delegate: Arc, row_delegate: Arc, ) -> FlowyResult> { - let configuration_reader = GroupConfigurationReaderImpl(pad.clone()); + let configuration_reader = GroupConfigurationReaderImpl(view_rev_pad.clone()); let field_revs = field_delegate.get_field_revs().await; + let layout = view_rev_pad.read().await.layout(); // Read the group field or find a new group field let field_rev = configuration_reader .get_configuration() @@ -391,24 +393,24 @@ async fn new_group_controller( .find(|field_rev| field_rev.id == configuration.field_id) .cloned() }) - .unwrap_or_else(|| find_group_field(&field_revs).unwrap()); + .unwrap_or_else(|| find_group_field(&field_revs, &layout).unwrap()); - new_group_controller_with_field_rev(user_id, view_id, pad, rev_manager, field_rev, row_delegate).await + new_group_controller_with_field_rev(user_id, view_id, view_rev_pad, rev_manager, field_rev, row_delegate).await } async fn new_group_controller_with_field_rev( user_id: String, view_id: String, - pad: Arc>, + view_rev_pad: Arc>, rev_manager: Arc, field_rev: Arc, row_delegate: Arc, ) -> FlowyResult> { - let configuration_reader = GroupConfigurationReaderImpl(pad.clone()); + let configuration_reader = GroupConfigurationReaderImpl(view_rev_pad.clone()); let configuration_writer = GroupConfigurationWriterImpl { user_id, rev_manager, - view_pad: pad, + view_pad: view_rev_pad, }; let row_revs = row_delegate.gv_row_revs().await; make_group_controller(view_id, field_rev, row_revs, configuration_reader, configuration_writer).await diff --git a/frontend/rust-lib/flowy-grid/src/services/group/group_util.rs b/frontend/rust-lib/flowy-grid/src/services/group/group_util.rs index 50d93396d7..c15717e2a7 100644 --- a/frontend/rust-lib/flowy-grid/src/services/group/group_util.rs +++ b/frontend/rust-lib/flowy-grid/src/services/group/group_util.rs @@ -8,7 +8,7 @@ use crate::services::group::{ use flowy_error::FlowyResult; use flowy_grid_data_model::revision::{ CheckboxGroupConfigurationRevision, DateGroupConfigurationRevision, FieldRevision, GroupConfigurationRevision, - NumberGroupConfigurationRevision, RowRevision, SelectOptionGroupConfigurationRevision, + LayoutRevision, NumberGroupConfigurationRevision, RowRevision, SelectOptionGroupConfigurationRevision, TextGroupConfigurationRevision, UrlGroupConfigurationRevision, }; use std::sync::Arc; @@ -62,15 +62,17 @@ where Ok(group_controller) } -pub fn find_group_field(field_revs: &[Arc]) -> Option> { - let field_rev = field_revs - .iter() - .find(|field_rev| { - let field_type: FieldType = field_rev.ty.into(); - field_type.can_be_group() - }) - .cloned(); - field_rev +pub fn find_group_field(field_revs: &[Arc], layout: &LayoutRevision) -> Option> { + match layout { + LayoutRevision::Table => field_revs.iter().find(|field_rev| field_rev.is_primary).cloned(), + LayoutRevision::Board => field_revs + .iter() + .find(|field_rev| { + let field_type: FieldType = field_rev.ty.into(); + field_type.can_be_group() + }) + .cloned(), + } } pub fn default_group_configuration(field_rev: &FieldRevision) -> GroupConfigurationRevision { 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 d373c6dc2d..91cbfc2355 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 @@ -7,6 +7,7 @@ use flowy_folder::{ event_map::{FolderCouldServiceV1, WorkspaceDatabase, WorkspaceUser}, manager::FolderManager, }; +use flowy_grid::entities::GridLayout; use flowy_grid::manager::{make_grid_view_data, GridManager}; use flowy_grid::util::{make_default_board, make_default_grid}; use flowy_grid_data_model::revision::BuildGridContext; @@ -142,7 +143,15 @@ impl ViewDataProcessor for TextBlockViewDataProcessor { FutureResult::new(async move { manager.init() }) } - fn create_container(&self, user_id: &str, view_id: &str, delta_data: Bytes) -> FutureResult<(), FlowyError> { + fn create_container( + &self, + user_id: &str, + view_id: &str, + layout: ViewLayoutTypePB, + delta_data: Bytes, + ) -> FutureResult<(), FlowyError> { + // Only accept Document type + debug_assert_eq!(layout, ViewLayoutTypePB::Document); let repeated_revision: RepeatedRevision = Revision::initial_revision(user_id, view_id, delta_data).into(); let view_id = view_id.to_string(); let manager = self.0.clone(); @@ -196,7 +205,9 @@ impl ViewDataProcessor for TextBlockViewDataProcessor { _user_id: &str, _view_id: &str, data: Vec, + layout: ViewLayoutTypePB, ) -> FutureResult { + debug_assert_eq!(layout, ViewLayoutTypePB::Document); FutureResult::new(async move { Ok(Bytes::from(data)) }) } @@ -211,7 +222,13 @@ impl ViewDataProcessor for GridViewDataProcessor { FutureResult::new(async { Ok(()) }) } - fn create_container(&self, user_id: &str, view_id: &str, delta_data: Bytes) -> FutureResult<(), FlowyError> { + fn create_container( + &self, + user_id: &str, + view_id: &str, + _layout: ViewLayoutTypePB, + delta_data: Bytes, + ) -> FutureResult<(), FlowyError> { let repeated_revision: RepeatedRevision = Revision::initial_revision(user_id, view_id, delta_data).into(); let view_id = view_id.to_string(); let grid_manager = self.0.clone(); @@ -246,19 +263,22 @@ impl ViewDataProcessor for GridViewDataProcessor { view_id: &str, layout: ViewLayoutTypePB, ) -> FutureResult { - let build_context = match layout { - ViewLayoutTypePB::Grid => make_default_grid(), - ViewLayoutTypePB::Board => make_default_board(), + let (build_context, layout) = match layout { + ViewLayoutTypePB::Grid => (make_default_grid(), GridLayout::Table), + ViewLayoutTypePB::Board => (make_default_board(), GridLayout::Board), ViewLayoutTypePB::Document => { return FutureResult::new(async move { Err(FlowyError::internal().context(format!("Can't handle {:?} layout type", layout))) }); } }; + let user_id = user_id.to_string(); let view_id = view_id.to_string(); let grid_manager = self.0.clone(); - FutureResult::new(async move { make_grid_view_data(&user_id, &view_id, grid_manager, build_context).await }) + FutureResult::new( + async move { make_grid_view_data(&user_id, &view_id, layout, grid_manager, build_context).await }, + ) } fn create_view_from_delta_data( @@ -266,15 +286,26 @@ impl ViewDataProcessor for GridViewDataProcessor { user_id: &str, view_id: &str, data: Vec, + layout: ViewLayoutTypePB, ) -> FutureResult { let user_id = user_id.to_string(); let view_id = view_id.to_string(); let grid_manager = self.0.clone(); + let layout = match layout { + ViewLayoutTypePB::Grid => GridLayout::Table, + ViewLayoutTypePB::Board => GridLayout::Board, + ViewLayoutTypePB::Document => { + return FutureResult::new(async move { + Err(FlowyError::internal().context(format!("Can't handle {:?} layout type", layout))) + }); + } + }; + FutureResult::new(async move { let bytes = Bytes::from(data); let build_context = BuildGridContext::try_from(bytes)?; - make_grid_view_data(&user_id, &view_id, grid_manager, build_context).await + make_grid_view_data(&user_id, &view_id, layout, grid_manager, build_context).await }) } 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 74c3c72511..24b991be6a 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 @@ -48,11 +48,11 @@ pub struct GridViewRevision { } impl GridViewRevision { - pub fn new(grid_id: String, view_id: String) -> Self { + pub fn new(grid_id: String, view_id: String, layout: LayoutRevision) -> Self { GridViewRevision { view_id, grid_id, - layout: Default::default(), + layout, filters: Default::default(), groups: Default::default(), // row_orders: 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 5be920ec5e..cfa47f9d9c 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,5 +1,5 @@ use crate::entities::revision::{md5, RepeatedRevision, Revision}; -use crate::errors::{internal_error, CollaborateError, CollaborateResult}; +use crate::errors::{internal_error, CollaborateError, CollaborateResult }; use crate::util::{cal_diff, make_text_delta_from_revisions}; use bytes::Bytes; use flowy_grid_data_model::revision::{ @@ -103,8 +103,12 @@ impl GridRevisionPad { |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(())) + if grid_meta.fields[index].is_primary { + Err(CollaborateError::can_not_delete_primary_field()) + } else { + grid_meta.fields.remove(index); + Ok(Some(())) + } } }, ) 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 652c6d6867..8613418c40 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 @@ -3,7 +3,7 @@ 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, + GroupConfigurationRevision, GroupConfigurationsByFieldId, LayoutRevision, }; use lib_ot::core::{Delta, DeltaBuilder, EmptyAttributes, OperationTransform}; use std::sync::Arc; @@ -25,8 +25,8 @@ impl std::ops::Deref for GridViewRevisionPad { impl GridViewRevisionPad { // For the moment, the view_id is equal to grid_id. The grid_id represents the database id. // A database can be referenced by multiple views. - pub fn new(grid_id: String, view_id: String) -> Self { - let view = Arc::new(GridViewRevision::new(grid_id, view_id)); + pub fn new(grid_id: String, view_id: String, layout: LayoutRevision) -> Self { + let view = Arc::new(GridViewRevision::new(grid_id, view_id, layout)); let json = serde_json::to_string(&view).unwrap(); let delta = DeltaBuilder::new().insert(&json).build(); Self { view, delta } @@ -34,7 +34,11 @@ impl GridViewRevisionPad { pub fn from_delta(view_id: &str, delta: Delta) -> CollaborateResult { if delta.is_empty() { - return Ok(GridViewRevisionPad::new(view_id.to_owned(), view_id.to_owned())); + return Ok(GridViewRevisionPad::new( + view_id.to_owned(), + view_id.to_owned(), + LayoutRevision::Table, + )); } let s = delta.content()?; let view: GridViewRevision = serde_json::from_str(&s).map_err(|e| { @@ -163,6 +167,10 @@ impl GridViewRevisionPad { make_grid_view_rev_json_str(&self.view) } + pub fn layout(&self) -> LayoutRevision { + self.layout.clone() + } + fn modify(&mut self, f: F) -> CollaborateResult> where F: FnOnce(&mut GridViewRevision) -> CollaborateResult>, diff --git a/shared-lib/flowy-sync/src/errors.rs b/shared-lib/flowy-sync/src/errors.rs index bc498e4d70..62f9519e63 100644 --- a/shared-lib/flowy-sync/src/errors.rs +++ b/shared-lib/flowy-sync/src/errors.rs @@ -1,7 +1,7 @@ use std::{fmt, fmt::Debug}; use strum_macros::Display; -macro_rules! static_doc_error { +macro_rules! static_error { ($name:ident, $status:expr) => { #[allow(non_snake_case, missing_docs)] pub fn $name() -> CollaborateError { @@ -34,12 +34,13 @@ impl CollaborateError { self } - static_doc_error!(internal, ErrorCode::InternalError); - static_doc_error!(undo, ErrorCode::UndoFail); - static_doc_error!(redo, ErrorCode::RedoFail); - static_doc_error!(out_of_bound, ErrorCode::OutOfBound); - static_doc_error!(record_not_found, ErrorCode::RecordNotFound); - static_doc_error!(revision_conflict, ErrorCode::RevisionConflict); + static_error!(internal, ErrorCode::InternalError); + static_error!(undo, ErrorCode::UndoFail); + static_error!(redo, ErrorCode::RedoFail); + static_error!(out_of_bound, ErrorCode::OutOfBound); + static_error!(record_not_found, ErrorCode::RecordNotFound); + static_error!(revision_conflict, ErrorCode::RevisionConflict); + static_error!(can_not_delete_primary_field, ErrorCode::CannotDeleteThePrimaryField); } impl fmt::Display for CollaborateError { @@ -57,6 +58,7 @@ pub enum ErrorCode { OutOfBound = 202, RevisionConflict = 203, RecordNotFound = 300, + CannotDeleteThePrimaryField = 301, InternalError = 1000, } From 082b0d2f5d8e4e0a0bf542725cef290df3e77e1e Mon Sep 17 00:00:00 2001 From: appflowy Date: Fri, 23 Sep 2022 13:43:48 +0800 Subject: [PATCH 06/25] fix: switch format style of the date after creating --- .../cell/cell_service/cell_controller.dart | 24 ++++++++----------- .../widgets/cell/date_cell/date_cell.dart | 4 ++-- .../widgets/cell/url_cell/url_cell.dart | 11 ++++----- 3 files changed, 17 insertions(+), 22 deletions(-) diff --git a/frontend/app_flowy/lib/plugins/grid/application/cell/cell_service/cell_controller.dart b/frontend/app_flowy/lib/plugins/grid/application/cell/cell_service/cell_controller.dart index 29fb521976..cf578f8b6c 100644 --- a/frontend/app_flowy/lib/plugins/grid/application/cell/cell_service/cell_controller.dart +++ b/frontend/app_flowy/lib/plugins/grid/application/cell/cell_service/cell_controller.dart @@ -149,18 +149,13 @@ class IGridCellController extends Equatable { _cellDataPersistence = cellDataPersistence, _fieldNotifier = fieldNotifier, _fieldService = FieldService( - gridId: cellId.gridId, fieldId: cellId.fieldContext.id), + gridId: cellId.gridId, + fieldId: cellId.fieldContext.id, + ), _cacheKey = GridCellCacheKey( - rowId: cellId.rowId, fieldId: cellId.fieldContext.id); - - IGridCellController clone() { - return IGridCellController( - cellId: cellId, - cellDataLoader: _cellDataLoader, - cellCache: _cellsCache, - fieldNotifier: _fieldNotifier, - cellDataPersistence: _cellDataPersistence); - } + rowId: cellId.rowId, + fieldId: cellId.fieldContext.id, + ); String get gridId => cellId.gridId; @@ -172,9 +167,10 @@ class IGridCellController extends Equatable { FieldType get fieldType => cellId.fieldContext.fieldType; - VoidCallback? startListening( - {required void Function(T?) onCellChanged, - VoidCallback? onCellFieldChanged}) { + VoidCallback? startListening({ + required void Function(T?) onCellChanged, + VoidCallback? onCellFieldChanged, + }) { if (isListening) { Log.error("Already started. It seems like you should call clone first"); return null; 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 d55ad8ada0..f90b8d06f1 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 @@ -80,9 +80,9 @@ class _DateCellState extends GridCellState { ), ), popupBuilder: (BuildContext popoverContent) { - final bloc = context.read(); return DateCellEditor( - cellController: bloc.cellController.clone(), + cellController: widget.cellControllerBuilder.build() + as GridDateCellController, onDismissed: () => widget.onCellEditing.value = false, ); }, 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 16f96421d8..5ed184aafb 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 @@ -54,13 +54,11 @@ class GridURLCell extends GridCellWidget { GridURLCellAccessoryType ty, GridCellAccessoryBuildContext buildContext) { switch (ty) { case GridURLCellAccessoryType.edit: - final cellController = - cellControllerBuilder.build() as GridURLCellController; return GridCellAccessoryBuilder( builder: (Key key) => _EditURLAccessory( key: key, - cellContext: cellController, anchorContext: buildContext.anchorContext, + cellControllerBuilder: cellControllerBuilder, ), ); @@ -191,10 +189,10 @@ class _GridURLCellState extends GridCellState { } class _EditURLAccessory extends StatefulWidget { - final GridURLCellController cellContext; + final GridCellControllerBuilder cellControllerBuilder; final BuildContext anchorContext; const _EditURLAccessory({ - required this.cellContext, + required this.cellControllerBuilder, required this.anchorContext, Key? key, }) : super(key: key); @@ -224,7 +222,8 @@ class _EditURLAccessoryState extends State<_EditURLAccessory> child: svgWidget("editor/edit", color: theme.iconColor), popupBuilder: (BuildContext popoverContext) { return URLEditorPopover( - cellController: widget.cellContext.clone(), + cellController: + widget.cellControllerBuilder.build() as GridURLCellController, ); }, ); From af98febcb0e993868f48ac0640075cb0426142c3 Mon Sep 17 00:00:00 2001 From: appflowy Date: Fri, 23 Sep 2022 15:10:20 +0800 Subject: [PATCH 07/25] fix: can't find context after popover disappear --- .../header/field_cell_action_sheet.dart | 34 +++++++++++-------- .../appflowy_popover/lib/src/popover.dart | 5 ++- 2 files changed, 22 insertions(+), 17 deletions(-) diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/field_cell_action_sheet.dart b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/field_cell_action_sheet.dart index 398a2206ae..5dca885414 100644 --- a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/field_cell_action_sheet.dart +++ b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/field_cell_action_sheet.dart @@ -93,9 +93,9 @@ class _EditFieldButton extends StatelessWidget { } class _FieldOperationList extends StatelessWidget { - final GridFieldCellContext fieldData; + final GridFieldCellContext fieldContext; final VoidCallback onDismissed; - const _FieldOperationList(this.fieldData, this.onDismissed, {Key? key}) + const _FieldOperationList(this.fieldContext, this.onDismissed, {Key? key}) : super(key: key); @override @@ -118,14 +118,14 @@ class _FieldOperationList extends StatelessWidget { bool enable = true; switch (action) { case FieldAction.delete: - enable = !fieldData.field.isPrimary; + enable = !fieldContext.field.isPrimary; break; default: break; } return FieldActionCell( - fieldId: fieldData.field.id, + fieldContext: fieldContext, action: action, onTap: onDismissed, enable: enable, @@ -136,13 +136,13 @@ class _FieldOperationList extends StatelessWidget { } class FieldActionCell extends StatelessWidget { - final String fieldId; + final GridFieldCellContext fieldContext; final VoidCallback onTap; final FieldAction action; final bool enable; const FieldActionCell({ - required this.fieldId, + required this.fieldContext, required this.action, required this.onTap, required this.enable, @@ -161,7 +161,7 @@ class FieldActionCell extends StatelessWidget { hoverColor: theme.hover, onTap: () { if (enable) { - action.run(context); + action.run(context, fieldContext); onTap(); } }, @@ -202,7 +202,7 @@ extension _FieldActionExtension on FieldAction { } } - void run(BuildContext context) { + void run(BuildContext context, GridFieldCellContext fieldContext) { switch (this) { case FieldAction.hide: context @@ -210,18 +210,24 @@ extension _FieldActionExtension on FieldAction { .add(const FieldActionSheetEvent.hideField()); break; case FieldAction.duplicate: - context - .read() - .add(const FieldActionSheetEvent.duplicateField()); + PopoverContainer.of(context).close(); + + FieldService( + gridId: fieldContext.gridId, + fieldId: fieldContext.field.id, + ).duplicateField(); + break; case FieldAction.delete: PopoverContainer.of(context).close(); + NavigatorAlertDialog( title: LocaleKeys.grid_field_deleteFieldPromptMessage.tr(), confirm: () { - context - .read() - .add(const FieldActionSheetEvent.deleteField()); + FieldService( + gridId: fieldContext.gridId, + fieldId: fieldContext.field.id, + ).deleteField(); }, ).show(context); diff --git a/frontend/app_flowy/packages/appflowy_popover/lib/src/popover.dart b/frontend/app_flowy/packages/appflowy_popover/lib/src/popover.dart index a817572c66..6b666c278c 100644 --- a/frontend/app_flowy/packages/appflowy_popover/lib/src/popover.dart +++ b/frontend/app_flowy/packages/appflowy_popover/lib/src/popover.dart @@ -136,7 +136,6 @@ class PopoverState extends State { return Stack(children: children); }); - _rootEntry.addEntry(context, this, newEntry, widget.asBarrier); } @@ -243,7 +242,7 @@ class PopoverContainerState extends State { ); } - close() => widget.onClose(); + void close() => widget.onClose(); - closeAll() => widget.onCloseAll(); + void closeAll() => widget.onCloseAll(); } From 8da2d24d4ef8fe4bc2b4a61839a3680e910c72a8 Mon Sep 17 00:00:00 2001 From: appflowy Date: Fri, 23 Sep 2022 15:22:22 +0800 Subject: [PATCH 08/25] fix: unit test --- frontend/rust-lib/flowy-grid/tests/grid/grid_editor.rs | 1 + shared-lib/flowy-sync/src/client_grid/grid_revision_pad.rs | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) 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 2bbf5874aa..eca0a17e16 100644 --- a/frontend/rust-lib/flowy-grid/tests/grid/grid_editor.rs +++ b/frontend/rust-lib/flowy-grid/tests/grid/grid_editor.rs @@ -127,6 +127,7 @@ fn make_test_grid() -> BuildGridContext { let text_field = FieldBuilder::new(RichTextTypeOptionBuilder::default()) .name("Name") .visibility(true) + .primary(true) .build(); grid_builder.add_field(text_field); } 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 cfa47f9d9c..64197716bb 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,5 +1,5 @@ use crate::entities::revision::{md5, RepeatedRevision, Revision}; -use crate::errors::{internal_error, CollaborateError, CollaborateResult }; +use crate::errors::{internal_error, CollaborateError, CollaborateResult}; use crate::util::{cal_diff, make_text_delta_from_revisions}; use bytes::Bytes; use flowy_grid_data_model::revision::{ From ee7cb0cb4214c613f89ad603a15a8fbba9c81602 Mon Sep 17 00:00:00 2001 From: appflowy Date: Fri, 23 Sep 2022 16:23:31 +0800 Subject: [PATCH 09/25] chore: delete button ui --- .../grid/presentation/widgets/header/field_editor.dart | 5 +++-- .../plugins/grid/presentation/widgets/row/row_detail.dart | 1 + 2 files changed, 4 insertions(+), 2 deletions(-) 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 1b3e579029..b6266e06e7 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 @@ -236,9 +236,10 @@ class _DeleteFieldButton extends StatelessWidget { color: enable ? null : theme.shader4, ), onTap: () => onDeleted?.call(), + hoverColor: theme.hover, + onHover: (_) => popoverMutex.close(), ); - // if (enable) button = button; - return button; + return SizedBox(height: 36, child: button); }, ); } 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 13a71166d8..51a161be57 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 @@ -280,6 +280,7 @@ class _RowDetailCellState extends State<_RowDetailCell> { AppFlowyPopover( controller: popover, constraints: BoxConstraints.loose(const Size(240, 600)), + triggerActions: PopoverTriggerFlags.none, popupBuilder: (popoverContext) => buildFieldEditor(), child: SizedBox( width: 150, From 0218f4e8b9ac6a21d229d92505fc76267789a058 Mon Sep 17 00:00:00 2001 From: appflowy Date: Fri, 23 Sep 2022 17:27:30 +0800 Subject: [PATCH 10/25] fix: create view panel icon color --- .../plugins/board/presentation/card/card.dart | 3 +- .../app_flowy/lib/plugins/doc/document.dart | 2 +- .../widgets/cell/date_cell/date_editor.dart | 82 ++++++++++++++----- .../widgets/header/field_editor.dart | 21 ++--- .../menu/app/header/right_click_action.dart | 12 ++- .../menu/app/section/disclosure_action.dart | 4 +- .../home/menu/app/section/item.dart | 8 +- .../widgets/float_bubble/question_bubble.dart | 17 ++-- .../presentation/widgets/pop_up_action.dart | 10 +-- .../src/flowy_overlay/appflowy_popover.dart | 2 +- .../controller_impls/default_controller.rs | 4 +- 11 files changed, 104 insertions(+), 61 deletions(-) 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 6e555b87c7..2580078e2d 100644 --- a/frontend/app_flowy/lib/plugins/board/presentation/card/card.dart +++ b/frontend/app_flowy/lib/plugins/board/presentation/card/card.dart @@ -133,7 +133,8 @@ class _BoardCardState extends State { throw UnimplementedError(); case AccessoryType.more: return GridRowActionSheet( - rowData: context.read().rowInfo()); + rowData: context.read().rowInfo(), + ); } } diff --git a/frontend/app_flowy/lib/plugins/doc/document.dart b/frontend/app_flowy/lib/plugins/doc/document.dart index 128eabfd20..9ef30f037e 100644 --- a/frontend/app_flowy/lib/plugins/doc/document.dart +++ b/frontend/app_flowy/lib/plugins/doc/document.dart @@ -233,7 +233,7 @@ class ShareActionWrapper extends ActionItem { ShareActionWrapper(this.inner); @override - Widget? get icon => null; + Widget? icon(Color iconColor) => null; @override String get name => inner.name; 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 f478c8cc2b..8ae4baf20e 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 @@ -76,7 +76,7 @@ class _DateCellEditor extends State { } } -class _CellCalendarWidget extends StatelessWidget { +class _CellCalendarWidget extends StatefulWidget { final GridDateCellController cellContext; final DateTypeOptionPB dateTypeOptionPB; @@ -86,26 +86,43 @@ class _CellCalendarWidget extends StatelessWidget { Key? key, }) : super(key: key); + @override + State<_CellCalendarWidget> createState() => _CellCalendarWidgetState(); +} + +class _CellCalendarWidgetState extends State<_CellCalendarWidget> { + late PopoverMutex popoverMutex; + late DateCalBloc bloc; + + @override + void initState() { + popoverMutex = PopoverMutex(); + + bloc = DateCalBloc( + dateTypeOptionPB: widget.dateTypeOptionPB, + cellData: widget.cellContext.getCellData(), + cellController: widget.cellContext, + )..add(const DateCalEvent.initial()); + super.initState(); + } + @override Widget build(BuildContext context) { final theme = context.watch(); - return BlocProvider( - create: (context) { - return DateCalBloc( - dateTypeOptionPB: dateTypeOptionPB, - cellData: cellContext.getCellData(), - cellController: cellContext, - )..add(const DateCalEvent.initial()); - }, + return BlocProvider.value( + value: bloc, child: BlocBuilder( buildWhen: (p, c) => false, builder: (context, state) { List children = [ _buildCalendar(theme, context), - _TimeTextField(bloc: context.read()), + _TimeTextField( + bloc: context.read(), + popoverMutex: popoverMutex, + ), Divider(height: 1, color: theme.shader5), const _IncludeTimeButton(), - const _DateTypeOptionButton() + _DateTypeOptionButton(popoverMutex: popoverMutex) ]; return ListView.separated( @@ -124,6 +141,13 @@ class _CellCalendarWidget extends StatelessWidget { ); } + @override + void dispose() { + bloc.close(); + popoverMutex.dispose(); + super.dispose(); + } + Widget _buildCalendar(AppTheme theme, BuildContext context) { return BlocBuilder( builder: (context, state) { @@ -223,8 +247,10 @@ class _IncludeTimeButton extends StatelessWidget { class _TimeTextField extends StatefulWidget { final DateCalBloc bloc; + final PopoverMutex popoverMutex; const _TimeTextField({ required this.bloc, + required this.popoverMutex, Key? key, }) : super(key: key); @@ -245,9 +271,18 @@ class _TimeTextFieldState extends State<_TimeTextField> { if (mounted) { widget.bloc.add(DateCalEvent.setTime(_controller.text)); } + + if (_focusNode.hasFocus) { + widget.popoverMutex.close(); + } + }); + + widget.popoverMutex.listenOnPopoverChanged(() { + if (_focusNode.hasFocus) { + _focusNode.unfocus(); + } }); } - super.initState(); } @@ -295,7 +330,11 @@ class _TimeTextFieldState extends State<_TimeTextField> { } class _DateTypeOptionButton extends StatelessWidget { - const _DateTypeOptionButton({Key? key}) : super(key: key); + final PopoverMutex popoverMutex; + const _DateTypeOptionButton({ + required this.popoverMutex, + Key? key, + }) : super(key: key); @override Widget build(BuildContext context) { @@ -306,6 +345,7 @@ class _DateTypeOptionButton extends StatelessWidget { selector: (state) => state.dateTypeOptionPB, builder: (context, dateTypeOptionPB) { return AppFlowyPopover( + mutex: popoverMutex, triggerActions: PopoverTriggerFlags.hover | PopoverTriggerFlags.click, offset: const Offset(20, 0), constraints: BoxConstraints.loose(const Size(140, 100)), @@ -318,6 +358,7 @@ class _DateTypeOptionButton extends StatelessWidget { popupBuilder: (BuildContext popContext) { return _CalDateTimeSetting( dateTypeOptionPB: dateTypeOptionPB, + popoverMutex: popoverMutex, onEvent: (event) => context.read().add(event), ); }, @@ -328,11 +369,15 @@ class _DateTypeOptionButton extends StatelessWidget { } class _CalDateTimeSetting extends StatefulWidget { + final PopoverMutex popoverMutex; final DateTypeOptionPB dateTypeOptionPB; final Function(DateCalEvent) onEvent; - const _CalDateTimeSetting( - {required this.dateTypeOptionPB, required this.onEvent, Key? key}) - : super(key: key); + const _CalDateTimeSetting({ + required this.dateTypeOptionPB, + required this.popoverMutex, + required this.onEvent, + Key? key, + }) : super(key: key); @override State<_CalDateTimeSetting> createState() => _CalDateTimeSettingState(); @@ -340,13 +385,12 @@ class _CalDateTimeSetting extends StatefulWidget { class _CalDateTimeSettingState extends State<_CalDateTimeSetting> { String? overlayIdentifier; - final _popoverMutex = PopoverMutex(); @override Widget build(BuildContext context) { List children = [ AppFlowyPopover( - mutex: _popoverMutex, + mutex: widget.popoverMutex, asBarrier: true, triggerActions: PopoverTriggerFlags.hover | PopoverTriggerFlags.click, offset: const Offset(20, 0), @@ -360,7 +404,7 @@ class _CalDateTimeSettingState extends State<_CalDateTimeSetting> { child: const DateFormatButton(), ), AppFlowyPopover( - mutex: _popoverMutex, + mutex: widget.popoverMutex, asBarrier: true, triggerActions: PopoverTriggerFlags.hover | PopoverTriggerFlags.click, offset: const Offset(20, 0), 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 b6266e06e7..6e55a345a2 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 @@ -139,7 +139,6 @@ class _FieldNameTextField extends StatefulWidget { class _FieldNameTextFieldState extends State<_FieldNameTextField> { FocusNode focusNode = FocusNode(); - VoidCallback? _popoverCallback; late TextEditingController controller; @override @@ -151,6 +150,12 @@ class _FieldNameTextFieldState extends State<_FieldNameTextField> { } }); + widget.popoverMutex.listenOnPopoverChanged(() { + if (focusNode.hasFocus) { + focusNode.unfocus(); + } + }); + super.initState(); } @@ -176,8 +181,6 @@ class _FieldNameTextFieldState extends State<_FieldNameTextField> { buildWhen: (previous, current) => previous.errorText != current.errorText, builder: (context, state) { - listenOnPopoverChanged(context); - return RoundedInputField( height: 36, focusNode: focusNode, @@ -198,18 +201,6 @@ class _FieldNameTextFieldState extends State<_FieldNameTextField> { ), ); } - - void listenOnPopoverChanged(BuildContext context) { - if (_popoverCallback != null) { - widget.popoverMutex.removePopoverListener(_popoverCallback!); - } - _popoverCallback = widget.popoverMutex.listenOnPopoverChanged(() { - if (focusNode.hasFocus) { - final node = FocusScope.of(context); - node.unfocus(); - } - }); - } } class _DeleteFieldButton extends StatelessWidget { diff --git a/frontend/app_flowy/lib/workspace/presentation/home/menu/app/header/right_click_action.dart b/frontend/app_flowy/lib/workspace/presentation/home/menu/app/header/right_click_action.dart index be6a97dbfb..821321c6ac 100644 --- a/frontend/app_flowy/lib/workspace/presentation/home/menu/app/header/right_click_action.dart +++ b/frontend/app_flowy/lib/workspace/presentation/home/menu/app/header/right_click_action.dart @@ -5,9 +5,12 @@ import 'package:flutter/material.dart'; import 'header.dart'; -class AppDisclosureActionSheet with ActionList, FlowyOverlayDelegate { +class AppDisclosureActionSheet + with ActionList, FlowyOverlayDelegate { final Function(dartz.Option) onSelected; - final _items = AppDisclosureAction.values.map((action) => DisclosureActionWrapper(action)).toList(); + final _items = AppDisclosureAction.values + .map((action) => DisclosureActionWrapper(action)) + .toList(); AppDisclosureActionSheet({ required this.onSelected, @@ -17,7 +20,8 @@ class AppDisclosureActionSheet with ActionList, FlowyOv List get items => _items; @override - void Function(dartz.Option p1) get selectCallback => (result) { + void Function(dartz.Option p1) get selectCallback => + (result) { result.fold( () => onSelected(dartz.none()), (wrapper) => onSelected( @@ -40,7 +44,7 @@ class DisclosureActionWrapper extends ActionItem { DisclosureActionWrapper(this.inner); @override - Widget? get icon => inner.icon; + Widget? icon(Color iconColor) => inner.icon; @override String get name => inner.name; 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 019f674b01..f420ec30e6 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,7 @@ class ViewDisclosureRegion extends StatelessWidget @override Widget build(BuildContext context) { return Listener( - onPointerDown: (event) => {_handleClick(event, context)}, + onPointerDown: (event) => _handleClick(event, context), child: child, ); } @@ -123,7 +123,7 @@ class ViewDisclosureActionWrapper extends ActionItem { ViewDisclosureActionWrapper(this.inner); @override - Widget? get icon => inner.icon; + Widget? icon(Color iconColor) => inner.icon(iconColor); @override String get name => inner.name; 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 9c9b5a0895..e66e69e414 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 @@ -147,14 +147,14 @@ extension ViewDisclosureExtension on ViewDisclosureAction { } } - Widget get icon { + Widget icon(Color iconColor) { switch (this) { case ViewDisclosureAction.rename: - return svgWidget('editor/edit', color: const Color(0xff999999)); + return svgWidget('editor/edit', color: iconColor); case ViewDisclosureAction.delete: - return svgWidget('editor/delete', color: const Color(0xff999999)); + return svgWidget('editor/delete', color: iconColor); case ViewDisclosureAction.duplicate: - return svgWidget('editor/copy', color: const Color(0xff999999)); + return svgWidget('editor/copy', color: iconColor); } } } diff --git a/frontend/app_flowy/lib/workspace/presentation/widgets/float_bubble/question_bubble.dart b/frontend/app_flowy/lib/workspace/presentation/widgets/float_bubble/question_bubble.dart index dbcafe2e44..67e5f4ba25 100644 --- a/frontend/app_flowy/lib/workspace/presentation/widgets/float_bubble/question_bubble.dart +++ b/frontend/app_flowy/lib/workspace/presentation/widgets/float_bubble/question_bubble.dart @@ -101,9 +101,11 @@ class _DebugToast { } } -class QuestionBubbleActionSheet with ActionList, FlowyOverlayDelegate { +class QuestionBubbleActionSheet + with ActionList, FlowyOverlayDelegate { final Function(dartz.Option) onSelected; - final _items = BubbleAction.values.map((action) => BubbleActionWrapper(action)).toList(); + final _items = + BubbleAction.values.map((action) => BubbleActionWrapper(action)).toList(); QuestionBubbleActionSheet({ required this.onSelected, @@ -119,7 +121,8 @@ class QuestionBubbleActionSheet with ActionList, FlowyOverl List get items => _items; @override - void Function(dartz.Option p1) get selectCallback => (result) { + void Function(dartz.Option p1) get selectCallback => + (result) { result.fold( () => onSelected(dartz.none()), (wrapper) => onSelected( @@ -156,7 +159,8 @@ class FlowyVersionDescription extends StatelessWidget { builder: (BuildContext context, AsyncSnapshot snapshot) { if (snapshot.connectionState == ConnectionState.done) { if (snapshot.hasError) { - return FlowyText("Error: ${snapshot.error}", fontSize: 12, color: theme.shader4); + return FlowyText("Error: ${snapshot.error}", + fontSize: 12, color: theme.shader4); } PackageInfo packageInfo = snapshot.data; @@ -170,7 +174,8 @@ class FlowyVersionDescription extends StatelessWidget { children: [ Divider(height: 1, color: theme.shader6, thickness: 1.0), const VSpace(6), - FlowyText("$appName $version.$buildNumber", fontSize: 12, color: theme.shader4), + FlowyText("$appName $version.$buildNumber", + fontSize: 12, color: theme.shader4), ], ).padding( horizontal: ActionListSizes.itemHPadding + ActionListSizes.padding, @@ -190,7 +195,7 @@ class BubbleActionWrapper extends ActionItem { BubbleActionWrapper(this.inner); @override - Widget? get icon => inner.emoji; + Widget? icon(Color iconColor) => inner.emoji; @override String get name => inner.name; diff --git a/frontend/app_flowy/lib/workspace/presentation/widgets/pop_up_action.dart b/frontend/app_flowy/lib/workspace/presentation/widgets/pop_up_action.dart index cbcec14fee..64f29036e0 100644 --- a/frontend/app_flowy/lib/workspace/presentation/widgets/pop_up_action.dart +++ b/frontend/app_flowy/lib/workspace/presentation/widgets/pop_up_action.dart @@ -59,7 +59,7 @@ abstract class ActionList { } abstract class ActionItem { - Widget? get icon; + Widget? icon(Color iconColor); String get name; } @@ -83,6 +83,7 @@ class ActionCell extends StatelessWidget { @override Widget build(BuildContext context) { final theme = context.watch(); + final icon = action.icon(theme.iconColor); return FlowyHover( style: HoverStyle(hoverColor: theme.hover), @@ -94,12 +95,9 @@ class ActionCell extends StatelessWidget { child: Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ - if (action.icon != null) action.icon!, + if (icon != null) icon, HSpace(ActionListSizes.itemHPadding), - FlowyText.medium( - action.name, - fontSize: 12, - ), + FlowyText.medium(action.name, fontSize: 12), ], ), ).padding( diff --git a/frontend/app_flowy/packages/flowy_infra_ui/lib/src/flowy_overlay/appflowy_popover.dart b/frontend/app_flowy/packages/flowy_infra_ui/lib/src/flowy_overlay/appflowy_popover.dart index 7885cbb01d..54a7b1941d 100644 --- a/frontend/app_flowy/packages/flowy_infra_ui/lib/src/flowy_overlay/appflowy_popover.dart +++ b/frontend/app_flowy/packages/flowy_infra_ui/lib/src/flowy_overlay/appflowy_popover.dart @@ -30,7 +30,7 @@ class AppFlowyPopover extends StatelessWidget { this.offset, this.controller, this.asBarrier = false, - this.margin = const EdgeInsets.all(12), + this.margin = const EdgeInsets.all(6), }) : super(key: key); @override diff --git a/frontend/rust-lib/flowy-grid/src/services/group/controller_impls/default_controller.rs b/frontend/rust-lib/flowy-grid/src/services/group/controller_impls/default_controller.rs index 2489df8af2..ddcdd7f3e1 100644 --- a/frontend/rust-lib/flowy-grid/src/services/group/controller_impls/default_controller.rs +++ b/frontend/rust-lib/flowy-grid/src/services/group/controller_impls/default_controller.rs @@ -55,7 +55,7 @@ impl GroupControllerSharedOperation for DefaultGroupController { _row_rev: &RowRevision, _field_rev: &FieldRevision, ) -> FlowyResult> { - todo!() + Ok(vec![]) } fn did_delete_row( @@ -63,7 +63,7 @@ impl GroupControllerSharedOperation for DefaultGroupController { _row_rev: &RowRevision, _field_rev: &FieldRevision, ) -> FlowyResult> { - todo!() + Ok(vec![]) } fn move_group_row(&mut self, _context: MoveGroupRowContext) -> FlowyResult> { From 9730069ec1755e489e9c15a23ae76e5f8a4e5a5e Mon Sep 17 00:00:00 2001 From: appflowy Date: Fri, 23 Sep 2022 17:40:17 +0800 Subject: [PATCH 11/25] chore: rounded button hover --- .../widgets/cell/date_cell/date_editor.dart | 30 ++++++++++--------- .../widgets/header/field_cell.dart | 1 + .../widgets/header/grid_header.dart | 1 + .../lib/style_widget/button.dart | 9 ++++-- 4 files changed, 25 insertions(+), 16 deletions(-) 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 8ae4baf20e..63917da8f9 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 @@ -358,8 +358,10 @@ class _DateTypeOptionButton extends StatelessWidget { popupBuilder: (BuildContext popContext) { return _CalDateTimeSetting( dateTypeOptionPB: dateTypeOptionPB, - popoverMutex: popoverMutex, - onEvent: (event) => context.read().add(event), + onEvent: (event) { + context.read().add(event); + popoverMutex.close(); + }, ); }, ); @@ -369,12 +371,10 @@ class _DateTypeOptionButton extends StatelessWidget { } class _CalDateTimeSetting extends StatefulWidget { - final PopoverMutex popoverMutex; final DateTypeOptionPB dateTypeOptionPB; final Function(DateCalEvent) onEvent; const _CalDateTimeSetting({ required this.dateTypeOptionPB, - required this.popoverMutex, required this.onEvent, Key? key, }) : super(key: key); @@ -384,36 +384,38 @@ class _CalDateTimeSetting extends StatefulWidget { } class _CalDateTimeSettingState extends State<_CalDateTimeSetting> { + final timeSettingPopoverMutex = PopoverMutex(); String? overlayIdentifier; @override Widget build(BuildContext context) { List children = [ AppFlowyPopover( - mutex: widget.popoverMutex, - asBarrier: true, + mutex: timeSettingPopoverMutex, triggerActions: PopoverTriggerFlags.hover | PopoverTriggerFlags.click, offset: const Offset(20, 0), popupBuilder: (BuildContext context) { return DateFormatList( selectedFormat: widget.dateTypeOptionPB.dateFormat, - onSelected: (format) => - widget.onEvent(DateCalEvent.setDateFormat(format)), + onSelected: (format) { + widget.onEvent(DateCalEvent.setDateFormat(format)); + timeSettingPopoverMutex.close(); + }, ); }, child: const DateFormatButton(), ), AppFlowyPopover( - mutex: widget.popoverMutex, - asBarrier: true, + mutex: timeSettingPopoverMutex, triggerActions: PopoverTriggerFlags.hover | PopoverTriggerFlags.click, offset: const Offset(20, 0), popupBuilder: (BuildContext context) { return TimeFormatList( - selectedFormat: widget.dateTypeOptionPB.timeFormat, - onSelected: (format) => - widget.onEvent(DateCalEvent.setTimeFormat(format)), - ); + selectedFormat: widget.dateTypeOptionPB.timeFormat, + onSelected: (format) { + widget.onEvent(DateCalEvent.setTimeFormat(format)); + timeSettingPopoverMutex.close(); + }); }, child: TimeFormatButton(timeFormat: widget.dateTypeOptionPB.timeFormat), ), 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 fd6133770f..80b94762ec 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 @@ -162,6 +162,7 @@ class FieldCellButton extends StatelessWidget { Widget build(BuildContext context) { final theme = context.watch(); return FlowyButton( + radius: BorderRadius.zero, hoverColor: theme.shader6, onTap: onTap, leftIcon: svgWidget(field.fieldType.iconName(), color: theme.iconColor), 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 dbd12119b5..735a179c1e 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 @@ -180,6 +180,7 @@ class CreateFieldButton extends StatelessWidget { asBarrier: true, constraints: BoxConstraints.loose(const Size(240, 600)), child: FlowyButton( + radius: BorderRadius.zero, text: FlowyText.medium( LocaleKeys.grid_field_newColumn.tr(), fontSize: 12, diff --git a/frontend/app_flowy/packages/flowy_infra_ui/lib/style_widget/button.dart b/frontend/app_flowy/packages/flowy_infra_ui/lib/style_widget/button.dart index ba2b3049af..159513dd49 100644 --- a/frontend/app_flowy/packages/flowy_infra_ui/lib/style_widget/button.dart +++ b/frontend/app_flowy/packages/flowy_infra_ui/lib/style_widget/button.dart @@ -12,6 +12,8 @@ class FlowyButton extends StatelessWidget { final Widget? rightIcon; final Color hoverColor; final bool isSelected; + final BorderRadius radius; + const FlowyButton({ Key? key, required this.text, @@ -22,6 +24,7 @@ class FlowyButton extends StatelessWidget { this.rightIcon, this.hoverColor = Colors.transparent, this.isSelected = false, + this.radius = const BorderRadius.all(Radius.circular(6)), }) : super(key: key); @override @@ -29,8 +32,10 @@ class FlowyButton extends StatelessWidget { return InkWell( onTap: onTap, child: FlowyHover( - style: - HoverStyle(borderRadius: BorderRadius.zero, hoverColor: hoverColor), + style: HoverStyle( + borderRadius: radius, + hoverColor: hoverColor, + ), onHover: onHover, setSelected: () => isSelected, builder: (context, onHover) => _render(), From 77d1dbedd6650761f72356905751d7d956688ab4 Mon Sep 17 00:00:00 2001 From: appflowy Date: Fri, 23 Sep 2022 17:50:32 +0800 Subject: [PATCH 12/25] chore: adjust insets of some popup --- .../app_flowy/lib/plugins/board/presentation/card/card.dart | 1 + .../lib/plugins/grid/presentation/widgets/row/grid_row.dart | 1 + .../plugins/grid/presentation/widgets/toolbar/grid_toolbar.dart | 1 + .../flowy_infra_ui/lib/src/flowy_overlay/appflowy_popover.dart | 2 +- 4 files changed, 4 insertions(+), 1 deletion(-) 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 2580078e2d..c08ece7474 100644 --- a/frontend/app_flowy/lib/plugins/board/presentation/card/card.dart +++ b/frontend/app_flowy/lib/plugins/board/presentation/card/card.dart @@ -85,6 +85,7 @@ class _BoardCardState extends State { controller: popoverController, triggerActions: PopoverTriggerFlags.none, constraints: BoxConstraints.loose(const Size(140, 200)), + margin: const EdgeInsets.all(6), direction: PopoverDirection.rightWithCenterAligned, popupBuilder: (popoverContext) => _handlePopoverBuilder( context, 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 2c3065196f..79643f48fb 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 @@ -109,6 +109,7 @@ class _RowLeadingState extends State<_RowLeading> { triggerActions: PopoverTriggerFlags.none, constraints: BoxConstraints.loose(const Size(140, 200)), direction: PopoverDirection.rightWithCenterAligned, + margin: const EdgeInsets.all(6), popupBuilder: (BuildContext popoverContext) { return GridRowActionSheet( rowData: context.read().state.rowInfo); 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 197dfb3dd5..7b5ca129ce 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 @@ -55,6 +55,7 @@ class _SettingButton extends StatelessWidget { return AppFlowyPopover( constraints: BoxConstraints.loose(const Size(260, 400)), offset: const Offset(0, 10), + margin: const EdgeInsets.all(6), child: FlowyIconButton( width: 22, hoverColor: theme.hover, diff --git a/frontend/app_flowy/packages/flowy_infra_ui/lib/src/flowy_overlay/appflowy_popover.dart b/frontend/app_flowy/packages/flowy_infra_ui/lib/src/flowy_overlay/appflowy_popover.dart index 54a7b1941d..7885cbb01d 100644 --- a/frontend/app_flowy/packages/flowy_infra_ui/lib/src/flowy_overlay/appflowy_popover.dart +++ b/frontend/app_flowy/packages/flowy_infra_ui/lib/src/flowy_overlay/appflowy_popover.dart @@ -30,7 +30,7 @@ class AppFlowyPopover extends StatelessWidget { this.offset, this.controller, this.asBarrier = false, - this.margin = const EdgeInsets.all(6), + this.margin = const EdgeInsets.all(12), }) : super(key: key); @override From f990e3cabbb006b887af71815702fc8f256d1498 Mon Sep 17 00:00:00 2001 From: appflowy Date: Fri, 23 Sep 2022 18:00:11 +0800 Subject: [PATCH 13/25] chore: set icon color of the app ation panel --- .../presentation/home/menu/app/header/header.dart | 9 +++++---- .../home/menu/app/header/right_click_action.dart | 2 +- 2 files changed, 6 insertions(+), 5 deletions(-) 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 48f4f0d399..727b18762b 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 @@ -86,7 +86,8 @@ class MenuAppHeader extends StatelessWidget { ?.toggle(), onSecondaryTap: () { final actionList = AppDisclosureActionSheet( - onSelected: (action) => _handleAction(context, action)); + onSelected: (action) => _handleAction(context, action), + ); actionList.show( context, anchorDirection: AnchorDirection.bottomWithCenterAligned, @@ -158,12 +159,12 @@ extension AppDisclosureExtension on AppDisclosureAction { } } - Widget get icon { + Widget icon(Color iconColor) { switch (this) { case AppDisclosureAction.rename: - return svgWidget('editor/edit', color: const Color(0xffe5e5e5)); + return svgWidget('editor/edit', color: iconColor); case AppDisclosureAction.delete: - return svgWidget('editor/delete', color: const Color(0xffe5e5e5)); + return svgWidget('editor/delete', color: iconColor); } } } diff --git a/frontend/app_flowy/lib/workspace/presentation/home/menu/app/header/right_click_action.dart b/frontend/app_flowy/lib/workspace/presentation/home/menu/app/header/right_click_action.dart index 821321c6ac..387f161732 100644 --- a/frontend/app_flowy/lib/workspace/presentation/home/menu/app/header/right_click_action.dart +++ b/frontend/app_flowy/lib/workspace/presentation/home/menu/app/header/right_click_action.dart @@ -44,7 +44,7 @@ class DisclosureActionWrapper extends ActionItem { DisclosureActionWrapper(this.inner); @override - Widget? icon(Color iconColor) => inner.icon; + Widget? icon(Color iconColor) => inner.icon(iconColor); @override String get name => inner.name; From c5af7db2cd90af323b3e459b06013d4f627fe272 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Thu, 22 Sep 2022 14:28:08 +0800 Subject: [PATCH 14/25] fix: could not input space in editor --- .../appflowy_editor/example/.firebaserc | 5 + .../appflowy_editor/example/firebase.json | 23 +++ .../appflowy_editor/example/lib/main.dart | 71 +++++--- .../example/lib/plugin/image_node_widget.dart | 165 ------------------ .../lib/plugin/youtube_link_node_widget.dart | 100 ----------- .../Flutter/GeneratedPluginRegistrant.swift | 2 - .../example/macos/Podfile.lock | 6 - .../appflowy_editor/example/pubspec.yaml | 4 +- .../lib/src/document/node.dart | 67 +++---- .../lib/src/document/selection.dart | 6 +- .../lib/src/extensions/path_extensions.dart | 30 ++++ .../src/operation/transaction_builder.dart | 4 +- .../rich_text/built_in_text_widget.dart | 56 ++++++ .../render/rich_text/bulleted_list_text.dart | 8 +- .../src/render/rich_text/checkbox_text.dart | 52 +----- .../lib/src/service/editor_service.dart | 6 + .../lib/src/service/input_service.dart | 28 ++- .../backspace_handler.dart | 81 ++++++--- ...er_without_shift_in_text_node_handler.dart | 47 ++--- .../tab_handler.dart | 34 ++++ .../lib/src/service/keyboard_service.dart | 11 +- .../lib/src/service/selection_service.dart | 33 +++- .../built_in_shortcut_events.dart | 6 + .../shortcut_event/shortcut_event.dart | 6 +- .../test/document/node_test.dart | 153 ++++++++++++++++ .../test/extensions/path_extensions_test.dart | 38 ++++ .../test/infra/test_editor.dart | 1 + .../backspace_handler_test.dart | 55 +++++- .../tab_handler_test.dart | 151 ++++++++++++++++ 29 files changed, 799 insertions(+), 450 deletions(-) create mode 100644 frontend/app_flowy/packages/appflowy_editor/example/.firebaserc create mode 100644 frontend/app_flowy/packages/appflowy_editor/example/firebase.json delete mode 100644 frontend/app_flowy/packages/appflowy_editor/example/lib/plugin/image_node_widget.dart delete mode 100644 frontend/app_flowy/packages/appflowy_editor/example/lib/plugin/youtube_link_node_widget.dart create mode 100644 frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/tab_handler.dart create mode 100644 frontend/app_flowy/packages/appflowy_editor/test/document/node_test.dart create mode 100644 frontend/app_flowy/packages/appflowy_editor/test/extensions/path_extensions_test.dart create mode 100644 frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/tab_handler_test.dart diff --git a/frontend/app_flowy/packages/appflowy_editor/example/.firebaserc b/frontend/app_flowy/packages/appflowy_editor/example/.firebaserc new file mode 100644 index 0000000000..06fcc074c4 --- /dev/null +++ b/frontend/app_flowy/packages/appflowy_editor/example/.firebaserc @@ -0,0 +1,5 @@ +{ + "projects": { + "default": "appflowy-editor" + } +} diff --git a/frontend/app_flowy/packages/appflowy_editor/example/firebase.json b/frontend/app_flowy/packages/appflowy_editor/example/firebase.json new file mode 100644 index 0000000000..bba899ee87 --- /dev/null +++ b/frontend/app_flowy/packages/appflowy_editor/example/firebase.json @@ -0,0 +1,23 @@ +{ + "hosting": { + "public": "build/web", + "ignore": [ + "firebase.json", + "**/.*", + "**/node_modules/**" + ], + "rewrites": [ + { + "source": "**", + "destination": "/index.html" + } + ], + "headers": [ { + "source": "**/*.@(png|jpg|jpeg|gif)", + "headers": [ { + "key": "Access-Control-Allow-Origin", + "value": "*" + } ] + } ] + } +} diff --git a/frontend/app_flowy/packages/appflowy_editor/example/lib/main.dart b/frontend/app_flowy/packages/appflowy_editor/example/lib/main.dart index ba4db4ae4c..fd5ccdeff3 100644 --- a/frontend/app_flowy/packages/appflowy_editor/example/lib/main.dart +++ b/frontend/app_flowy/packages/appflowy_editor/example/lib/main.dart @@ -1,13 +1,16 @@ import 'dart:convert'; import 'dart:io'; -import 'package:example/plugin/underscore_to_italic.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; + +import 'package:example/plugin/underscore_to_italic.dart'; +import 'package:file_picker/file_picker.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:google_fonts/google_fonts.dart'; - import 'package:path_provider/path_provider.dart'; +import 'package:universal_html/html.dart' as html; import 'package:appflowy_editor/appflowy_editor.dart'; @@ -112,6 +115,7 @@ class _MyHomePageState extends State { child: AppFlowyEditor( editorState: _editorState!, editorStyle: _editorStyle, + editable: true, shortcutEvents: [ underscoreToItalic, ], @@ -148,7 +152,7 @@ class _MyHomePageState extends State { ), ActionButton( icon: const Icon(Icons.import_export), - onPressed: () => _importDocument(), + onPressed: () async => await _importDocument(), ), ActionButton( icon: const Icon(Icons.color_lens), @@ -167,28 +171,53 @@ class _MyHomePageState extends State { void _exportDocument(EditorState editorState) async { final document = editorState.document.toJson(); final json = jsonEncode(document); - final directory = await getTemporaryDirectory(); - final path = directory.path; - final file = File('$path/editor.json'); - await file.writeAsString(json); + if (kIsWeb) { + final blob = html.Blob([json], 'text/plain', 'native'); + html.AnchorElement( + href: html.Url.createObjectUrlFromBlob(blob).toString(), + ) + ..setAttribute('download', 'editor.json') + ..click(); + } else { + final directory = await getTemporaryDirectory(); + final path = directory.path; + final file = File('$path/editor.json'); + await file.writeAsString(json); - if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text('The document is saved to the ${file.path}'), - ), - ); + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('The document is saved to the ${file.path}'), + ), + ); + } } } - void _importDocument() async { - final directory = await getTemporaryDirectory(); - final path = directory.path; - final file = File('$path/editor.json'); - setState(() { - _editorState = null; - _jsonString = file.readAsString(); - }); + Future _importDocument() async { + if (kIsWeb) { + final result = await FilePicker.platform.pickFiles( + allowMultiple: false, + allowedExtensions: ['json'], + type: FileType.custom, + ); + final bytes = result?.files.first.bytes; + if (bytes != null) { + final jsonString = const Utf8Decoder().convert(bytes); + setState(() { + _editorState = null; + _jsonString = Future.value(jsonString); + }); + } + } else { + final directory = await getTemporaryDirectory(); + final path = '${directory.path}/editor.json'; + final file = File(path); + setState(() { + _editorState = null; + _jsonString = file.readAsString(); + }); + } } void _switchToPage(int pageIndex) { diff --git a/frontend/app_flowy/packages/appflowy_editor/example/lib/plugin/image_node_widget.dart b/frontend/app_flowy/packages/appflowy_editor/example/lib/plugin/image_node_widget.dart deleted file mode 100644 index d76ce8d6a2..0000000000 --- a/frontend/app_flowy/packages/appflowy_editor/example/lib/plugin/image_node_widget.dart +++ /dev/null @@ -1,165 +0,0 @@ -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:flutter/material.dart'; - -/// 1. define your custom type in example.json -/// For example I need to define an image plugin, then I define type equals -/// "image", and add "image_src" into "attributes". -/// { -/// "type": "image", -/// "attributes", { "image_src": "https://s1.ax1x.com/2022/07/28/vCgz1x.png" } -/// } -/// 2. create a class extends [NodeWidgetBuilder] -/// 3. override the function `Widget build(NodeWidgetContext context)` -/// and return a widget to render. The returned widget should be -/// a StatefulWidget and mixin with [SelectableMixin]. -/// -/// 4. override the getter `nodeValidator` -/// to verify the data structure in [Node]. -/// 5. register the plugin with `type` to `AppFlowyEditor` in `main.dart`. -/// 6. Congratulations! - -class ImageNodeBuilder extends NodeWidgetBuilder { - @override - Widget build(NodeWidgetContext context) { - return ImageNodeWidget( - key: context.node.key, - node: context.node, - editorState: context.editorState, - ); - } - - @override - NodeValidator get nodeValidator => ((node) { - return node.type == 'image'; - }); -} - -const double placeholderHeight = 132; - -class ImageNodeWidget extends StatefulWidget { - final Node node; - final EditorState editorState; - - const ImageNodeWidget({ - Key? key, - required this.node, - required this.editorState, - }) : super(key: key); - - @override - State createState() => _ImageNodeWidgetState(); -} - -class _ImageNodeWidgetState extends State - with SelectableMixin { - bool isHovered = false; - Node get node => widget.node; - EditorState get editorState => widget.editorState; - String get src => widget.node.attributes['image_src'] as String; - - @override - Position end() { - return Position(path: node.path, offset: 0); - } - - @override - Position start() { - return Position(path: node.path, offset: 0); - } - - @override - List getRectsInSelection(Selection selection) { - return []; - } - - @override - Selection getSelectionInRange(Offset start, Offset end) { - return Selection.collapsed(Position(path: node.path, offset: 0)); - } - - @override - Offset localToGlobal(Offset offset) { - throw UnimplementedError(); - } - - @override - Position getPositionInOffset(Offset start) { - return Position(path: node.path, offset: 0); - } - - @override - Widget build(BuildContext context) { - 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: [ - 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, - ), - )), - ], - ); - } -} diff --git a/frontend/app_flowy/packages/appflowy_editor/example/lib/plugin/youtube_link_node_widget.dart b/frontend/app_flowy/packages/appflowy_editor/example/lib/plugin/youtube_link_node_widget.dart deleted file mode 100644 index 3923c60c45..0000000000 --- a/frontend/app_flowy/packages/appflowy_editor/example/lib/plugin/youtube_link_node_widget.dart +++ /dev/null @@ -1,100 +0,0 @@ -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:flutter/material.dart'; -import 'package:pod_player/pod_player.dart'; - -class YouTubeLinkNodeBuilder extends NodeWidgetBuilder { - @override - Widget build(NodeWidgetContext context) { - return LinkNodeWidget( - key: context.node.key, - node: context.node, - editorState: context.editorState, - ); - } - - @override - NodeValidator get nodeValidator => ((node) { - return node.type == 'youtube_link'; - }); -} - -class LinkNodeWidget extends StatefulWidget { - final Node node; - final EditorState editorState; - - const LinkNodeWidget({ - Key? key, - required this.node, - required this.editorState, - }) : super(key: key); - - @override - State createState() => _YouTubeLinkNodeWidgetState(); -} - -class _YouTubeLinkNodeWidgetState extends State - with SelectableMixin { - Node get node => widget.node; - EditorState get editorState => widget.editorState; - String get src => widget.node.attributes['youtube_link'] as String; - - @override - Position end() { - // TODO: implement end - throw UnimplementedError(); - } - - @override - Position start() { - // TODO: implement start - throw UnimplementedError(); - } - - @override - List getRectsInSelection(Selection selection) { - // TODO: implement getRectsInSelection - throw UnimplementedError(); - } - - @override - Selection getSelectionInRange(Offset start, Offset end) { - // TODO: implement getSelectionInRange - throw UnimplementedError(); - } - - @override - Offset localToGlobal(Offset offset) { - throw UnimplementedError(); - } - - @override - Position getPositionInOffset(Offset start) { - // TODO: implement getPositionInOffset - throw UnimplementedError(); - } - - @override - Widget build(BuildContext context) { - return _build(context); - } - - late final PodPlayerController controller; - - @override - void initState() { - controller = PodPlayerController( - playVideoFrom: PlayVideoFrom.network( - src, - ), - )..initialise(); - super.initState(); - } - - Widget _build(BuildContext context) { - return Column( - children: [ - PodVideoPlayer(controller: controller), - ], - ); - } -} diff --git a/frontend/app_flowy/packages/appflowy_editor/example/macos/Flutter/GeneratedPluginRegistrant.swift b/frontend/app_flowy/packages/appflowy_editor/example/macos/Flutter/GeneratedPluginRegistrant.swift index 08b7c3b866..f0f250ebd6 100644 --- a/frontend/app_flowy/packages/appflowy_editor/example/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/frontend/app_flowy/packages/appflowy_editor/example/macos/Flutter/GeneratedPluginRegistrant.swift @@ -8,11 +8,9 @@ import Foundation import path_provider_macos import rich_clipboard_macos import url_launcher_macos -import wakelock_macos func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) RichClipboardPlugin.register(with: registry.registrar(forPlugin: "RichClipboardPlugin")) UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) - WakelockMacosPlugin.register(with: registry.registrar(forPlugin: "WakelockMacosPlugin")) } diff --git a/frontend/app_flowy/packages/appflowy_editor/example/macos/Podfile.lock b/frontend/app_flowy/packages/appflowy_editor/example/macos/Podfile.lock index 1fcb47735c..49a5879fa6 100644 --- a/frontend/app_flowy/packages/appflowy_editor/example/macos/Podfile.lock +++ b/frontend/app_flowy/packages/appflowy_editor/example/macos/Podfile.lock @@ -6,15 +6,12 @@ PODS: - FlutterMacOS - url_launcher_macos (0.0.1): - FlutterMacOS - - wakelock_macos (0.0.1): - - FlutterMacOS DEPENDENCIES: - FlutterMacOS (from `Flutter/ephemeral`) - path_provider_macos (from `Flutter/ephemeral/.symlinks/plugins/path_provider_macos/macos`) - rich_clipboard_macos (from `Flutter/ephemeral/.symlinks/plugins/rich_clipboard_macos/macos`) - url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`) - - wakelock_macos (from `Flutter/ephemeral/.symlinks/plugins/wakelock_macos/macos`) EXTERNAL SOURCES: FlutterMacOS: @@ -25,15 +22,12 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral/.symlinks/plugins/rich_clipboard_macos/macos url_launcher_macos: :path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos - wakelock_macos: - :path: Flutter/ephemeral/.symlinks/plugins/wakelock_macos/macos SPEC CHECKSUMS: FlutterMacOS: 57701585bf7de1b3fc2bb61f6378d73bbdea8424 path_provider_macos: 3c0c3b4b0d4a76d2bf989a913c2de869c5641a19 rich_clipboard_macos: 43364b66b9dc69d203eb8dd6d758e2d12e02723c url_launcher_macos: 597e05b8e514239626bcf4a850fcf9ef5c856ec3 - wakelock_macos: bc3f2a9bd8d2e6c89fee1e1822e7ddac3bd004a9 PODFILE CHECKSUM: 6eac6b3292e5142cfc23bdeb71848a40ec51c14c diff --git a/frontend/app_flowy/packages/appflowy_editor/example/pubspec.yaml b/frontend/app_flowy/packages/appflowy_editor/example/pubspec.yaml index 7061a11226..3c3f51632e 100644 --- a/frontend/app_flowy/packages/appflowy_editor/example/pubspec.yaml +++ b/frontend/app_flowy/packages/appflowy_editor/example/pubspec.yaml @@ -37,12 +37,12 @@ dependencies: path: ../ provider: ^6.0.3 url_launcher: ^6.1.5 - video_player: ^2.4.5 - pod_player: 0.0.8 path_provider: ^2.0.11 google_fonts: ^3.0.1 flutter_localizations: sdk: flutter + file_picker: ^5.0.1 + universal_html: ^2.0.8 dev_dependencies: flutter_test: diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/document/node.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/document/node.dart index bfc40c4d32..81e87399b1 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/document/node.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/document/node.dart @@ -193,16 +193,24 @@ 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); + Node copyWith({ + String? type, + LinkedList? children, + Attributes? attributes, + }) { + final node = Node( + type: type ?? this.type, + attributes: attributes ?? {..._attributes}, + children: children ?? LinkedList(), + ); + if (children == null && this.children.isNotEmpty) { + for (final child in this.children) { + node.children.add( + child.copyWith()..parent = node, + ); + } } - return newNode; + return node; } } @@ -215,7 +223,10 @@ class TextNode extends Node { LinkedList? children, Attributes? attributes, }) : _delta = delta, - super(children: children ?? LinkedList(), attributes: attributes ?? {}); + super( + children: children ?? LinkedList(), + attributes: attributes ?? {}, + ); TextNode.empty({Attributes? attributes}) : _delta = Delta([TextInsert('')]), @@ -241,33 +252,27 @@ class TextNode extends Node { return map; } + @override TextNode copyWith({ String? type, LinkedList? children, Attributes? attributes, Delta? delta, - }) => - TextNode( - type: type ?? this.type, - children: children ?? this.children, - attributes: attributes ?? _attributes, - 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); + }) { + final textNode = TextNode( + type: type ?? this.type, + children: children, + attributes: attributes ?? _attributes, + delta: delta ?? this.delta, + ); + if (children == null && this.children.isNotEmpty) { + for (final child in this.children) { + textNode.children.add( + child.copyWith()..parent = textNode, + ); + } } - return newNode; + return textNode; } String toRawString() => _delta.toRawString(); diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/document/selection.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/document/selection.dart index 986dd37468..ea451b46dd 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/document/selection.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/document/selection.dart @@ -40,11 +40,9 @@ 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)) || - (isSingle && start.offset > end.offset); + (start.path > end.path) || (isSingle && start.offset > end.offset); bool get isBackward => - (start.path <= end.path && !pathEquals(start.path, end.path)) || - (isSingle && start.offset < end.offset); + (start.path < end.path) || (isSingle && start.offset < end.offset); Selection get normalize { if (isForward) { diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/extensions/path_extensions.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/extensions/path_extensions.dart index fb643443e4..b7955fe83a 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/extensions/path_extensions.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/extensions/path_extensions.dart @@ -4,22 +4,52 @@ import 'dart:math'; extension PathExtensions on Path { bool operator >=(Path other) { + if (pathEquals(this, other)) { + return true; + } + return this > other; + } + + bool operator >(Path other) { + if (pathEquals(this, other)) { + return false; + } final length = min(this.length, other.length); for (var i = 0; i < length; i++) { if (this[i] < other[i]) { return false; + } else if (this[i] > other[i]) { + return true; } } + if (this.length < other.length) { + return false; + } return true; } bool operator <=(Path other) { + if (pathEquals(this, other)) { + return true; + } + return this < other; + } + + bool operator <(Path other) { + if (pathEquals(this, other)) { + return false; + } final length = min(this.length, other.length); for (var i = 0; i < length; i++) { if (this[i] > other[i]) { return false; + } else if (this[i] < other[i]) { + return true; } } + if (this.length > other.length) { + return false; + } return true; } diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/operation/transaction_builder.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/operation/transaction_builder.dart index c990a3921f..7dfca6bcf7 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/operation/transaction_builder.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/operation/transaction_builder.dart @@ -36,7 +36,7 @@ class TransactionBuilder { /// Inserts a sequence of nodes at the position of path. insertNodes(Path path, List nodes) { beforeSelection = state.cursorSelection; - add(InsertOperation(path, nodes.map((node) => node.deepClone()).toList())); + add(InsertOperation(path, nodes.map((node) => node.copyWith()).toList())); } /// Updates the attributes of nodes. @@ -75,7 +75,7 @@ class TransactionBuilder { nodes.add(node); } - add(DeleteOperation(path, nodes.map((node) => node.deepClone()).toList())); + add(DeleteOperation(path, nodes.map((node) => node.copyWith()).toList())); } textEdit(TextNode node, Delta Function() f) { diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/built_in_text_widget.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/built_in_text_widget.dart index 954d5bd516..f2c8693f87 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/built_in_text_widget.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/built_in_text_widget.dart @@ -1,4 +1,5 @@ import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:appflowy_editor/src/render/rich_text/default_selectable.dart'; import 'package:flutter/material.dart'; abstract class BuiltInTextWidget extends StatefulWidget { @@ -59,3 +60,58 @@ mixin BuiltInStyleMixin on State { return const EdgeInsets.all(0); } } + +mixin BuiltInTextWidgetMixin on State + implements DefaultSelectable { + @override + Widget build(BuildContext context) { + if (widget.textNode.children.isEmpty) { + return buildWithSingle(context); + } else { + return buildWithChildren(context); + } + } + + Widget buildWithSingle(BuildContext context); + + Widget buildWithChildren(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + buildWithSingle(context), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // TODO: customize + const SizedBox( + width: 20, + ), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: widget.textNode.children + .map( + (child) => widget.editorState.service.renderPluginService + .buildPluginWidget( + child is TextNode + ? NodeWidgetContext( + context: context, + node: child, + editorState: widget.editorState, + ) + : NodeWidgetContext( + context: context, + node: child, + editorState: widget.editorState, + ), + ), + ) + .toList(), + ), + ) + ], + ) + ], + ); + } +} diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/bulleted_list_text.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/bulleted_list_text.dart index ac0534a5f4..5558b8934d 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/bulleted_list_text.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/bulleted_list_text.dart @@ -45,7 +45,11 @@ class BulletedListTextNodeWidget extends BuiltInTextWidget { // customize class _BulletedListTextNodeWidgetState extends State - with SelectableMixin, DefaultSelectable, BuiltInStyleMixin { + with + SelectableMixin, + DefaultSelectable, + BuiltInStyleMixin, + BuiltInTextWidgetMixin { @override final iconKey = GlobalKey(); @@ -61,7 +65,7 @@ class _BulletedListTextNodeWidgetState extends State } @override - Widget build(BuildContext context) { + Widget buildWithSingle(BuildContext context) { return Padding( padding: padding, child: Row( diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/checkbox_text.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/checkbox_text.dart index 0c6295f4ce..2ca7531d2b 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/checkbox_text.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/checkbox_text.dart @@ -46,7 +46,11 @@ class CheckboxNodeWidget extends BuiltInTextWidget { } class _CheckboxNodeWidgetState extends State - with SelectableMixin, DefaultSelectable, BuiltInStyleMixin { + with + SelectableMixin, + DefaultSelectable, + BuiltInStyleMixin, + BuiltInTextWidgetMixin { @override final iconKey = GlobalKey(); @@ -62,15 +66,7 @@ class _CheckboxNodeWidgetState extends State } @override - Widget build(BuildContext context) { - if (widget.textNode.children.isEmpty) { - return _buildWithSingle(context); - } else { - return _buildWithChildren(context); - } - } - - Widget _buildWithSingle(BuildContext context) { + Widget buildWithSingle(BuildContext context) { final check = widget.textNode.attributes.check; return Padding( padding: padding, @@ -106,40 +102,4 @@ class _CheckboxNodeWidgetState extends State ), ); } - - Widget _buildWithChildren(BuildContext context) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _buildWithSingle(context), - Row( - children: [ - const SizedBox( - width: 20, - ), - Column( - children: widget.textNode.children - .map( - (child) => widget.editorState.service.renderPluginService - .buildPluginWidget( - child is TextNode - ? NodeWidgetContext( - context: context, - node: child, - editorState: widget.editorState, - ) - : NodeWidgetContext( - context: context, - node: child, - editorState: widget.editorState, - ), - ), - ) - .toList(), - ) - ], - ) - ], - ); - } } diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/editor_service.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/editor_service.dart index 654d8e4b26..2655717c1d 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/editor_service.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/editor_service.dart @@ -38,6 +38,7 @@ class AppFlowyEditor extends StatefulWidget { this.customBuilders = const {}, this.shortcutEvents = const [], this.selectionMenuItems = const [], + this.editable = true, required this.editorStyle, }) : super(key: key); @@ -53,6 +54,8 @@ class AppFlowyEditor extends StatefulWidget { final EditorStyle editorStyle; + final bool editable; + @override State createState() => _AppFlowyEditorState(); } @@ -106,11 +109,14 @@ class _AppFlowyEditorState extends State { cursorColor: widget.editorStyle.cursorColor, selectionColor: widget.editorStyle.selectionColor, editorState: editorState, + editable: widget.editable, child: AppFlowyInput( key: editorState.service.inputServiceKey, editorState: editorState, + editable: widget.editable, child: AppFlowyKeyboard( key: editorState.service.keyboardServiceKey, + editable: widget.editable, shortcutEvents: [ ...builtInShortcutEvents, ...widget.shortcutEvents, diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/input_service.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/input_service.dart index a92fae1b95..7f1a4718f5 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/input_service.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/input_service.dart @@ -1,4 +1,5 @@ import 'package:appflowy_editor/src/infra/log.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -43,11 +44,13 @@ abstract class AppFlowyInputService { class AppFlowyInput extends StatefulWidget { const AppFlowyInput({ Key? key, + this.editable = true, required this.editorState, required this.child, }) : super(key: key); final EditorState editorState; + final bool editable; final Widget child; @override @@ -61,26 +64,39 @@ class _AppFlowyInputState extends State EditorState get _editorState => widget.editorState; + // Disable space shortcut on the Web platform. + final Map _shortcuts = kIsWeb + ? { + LogicalKeySet(LogicalKeyboardKey.space): + DoNothingAndStopPropagationIntent(), + } + : {}; + @override void initState() { super.initState(); - _editorState.service.selectionService.currentSelection - .addListener(_onSelectionChange); + if (widget.editable) { + _editorState.service.selectionService.currentSelection + .addListener(_onSelectionChange); + } } @override void dispose() { - close(); - _editorState.service.selectionService.currentSelection - .removeListener(_onSelectionChange); + if (widget.editable) { + close(); + _editorState.service.selectionService.currentSelection + .removeListener(_onSelectionChange); + } super.dispose(); } @override Widget build(BuildContext context) { - return Container( + return Shortcuts( + shortcuts: _shortcuts, child: widget.child, ); } diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/backspace_handler.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/backspace_handler.dart index 9d65088914..3df482012f 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/backspace_handler.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/backspace_handler.dart @@ -1,8 +1,8 @@ import 'package:appflowy_editor/src/service/internal_key_event_handlers/number_list_helper.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:appflowy_editor/src/document/built_in_attribute_keys.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:appflowy_editor/src/extensions/path_extensions.dart'; // Handle delete text. ShortcutEventHandler deleteTextHandler = (editorState, event) { @@ -121,32 +121,40 @@ KeyEventResult _handleBackspace(EditorState editorState, RawKeyEvent event) { } KeyEventResult _backDeleteToPreviousTextNode( - EditorState editorState, - TextNode textNode, - TransactionBuilder transactionBuilder, - List nonTextNodes, - Selection selection) { - var previous = textNode.previous; - bool prevIsNumberList = false; - while (previous != null) { - if (previous is TextNode) { - if (previous.subtype == BuiltInAttributeKey.numberList) { - prevIsNumberList = true; - } + EditorState editorState, + TextNode textNode, + TransactionBuilder transactionBuilder, + List nonTextNodes, + Selection selection, +) { + // Not reach to the root. + if (textNode.parent?.parent != null) { + transactionBuilder + ..deleteNode(textNode) + ..insertNode(textNode.parent!.path.next, textNode) + ..afterSelection = Selection.collapsed( + Position(path: textNode.parent!.path.next, offset: 0), + ) + ..commit(); + return KeyEventResult.handled; + } - transactionBuilder - ..mergeText(previous, textNode) - ..deleteNode(textNode) - ..afterSelection = Selection.collapsed( - Position( - path: previous.path, - offset: previous.toRawString().length, - ), - ); - break; - } else { - previous = previous.previous; + bool prevIsNumberList = false; + final previousTextNode = _closestTextNode(textNode.previous); + if (previousTextNode != null && previousTextNode is TextNode) { + if (previousTextNode.subtype == BuiltInAttributeKey.numberList) { + prevIsNumberList = true; } + + transactionBuilder + ..mergeText(previousTextNode, textNode) + ..deleteNode(textNode) + ..afterSelection = Selection.collapsed( + Position( + path: previousTextNode.path, + offset: previousTextNode.toRawString().length, + ), + ); } if (transactionBuilder.operations.isNotEmpty) { @@ -157,8 +165,8 @@ KeyEventResult _backDeleteToPreviousTextNode( } if (prevIsNumberList) { - makeFollowingNodesIncremental( - editorState, previous!.path, transactionBuilder.afterSelection!); + makeFollowingNodesIncremental(editorState, previousTextNode!.path, + transactionBuilder.afterSelection!); } return KeyEventResult.handled; @@ -261,3 +269,22 @@ void _deleteTextNodes(TransactionBuilder transactionBuilder, secondOffset: selection.end.offset, ); } + +// TODO: Just a simple solution for textNode, need to be optimized. +Node? _closestTextNode(Node? node) { + if (node is TextNode) { + var children = node.children; + if (children.isEmpty) { + return node; + } + var last = children.last; + while (last.children.isNotEmpty) { + last = children.last; + } + return last; + } + if (node?.previous != null) { + return _closestTextNode(node!.previous!); + } + return null; +} diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/enter_without_shift_in_text_node_handler.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/enter_without_shift_in_text_node_handler.dart index a82e751083..a981470f6e 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/enter_without_shift_in_text_node_handler.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/enter_without_shift_in_text_node_handler.dart @@ -1,9 +1,9 @@ +import 'dart:collection'; + import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:appflowy_editor/src/extensions/path_extensions.dart'; -import 'package:appflowy_editor/src/document/built_in_attribute_keys.dart'; import './number_list_helper.dart'; /// Handle some cases where enter is pressed and shift is not pressed. @@ -16,10 +16,6 @@ import './number_list_helper.dart'; /// 2.2 or insert a empty text node before. ShortcutEventHandler enterWithoutShiftInTextNodesHandler = (editorState, event) { - if (event.logicalKey != LogicalKeyboardKey.enter || event.isShiftPressed) { - return KeyEventResult.ignored; - } - var selection = editorState.service.selectionService.currentSelection.value; var nodes = editorState.service.selectionService.currentSelectedNodes; if (selection == null) { @@ -124,7 +120,10 @@ ShortcutEventHandler enterWithoutShiftInTextNodesHandler = TransactionBuilder(editorState) ..insertNode( textNode.path, - TextNode.empty(), + textNode.copyWith( + children: LinkedList(), + delta: Delta(), + ), ) ..afterSelection = afterSelection ..commit(); @@ -142,21 +141,25 @@ ShortcutEventHandler enterWithoutShiftInTextNodesHandler = Position(path: nextPath, offset: 0), ); - TransactionBuilder(editorState) - ..insertNode( - textNode.path.next, - textNode.copyWith( - attributes: attributes, - delta: textNode.delta.slice(selection.end.offset), - ), - ) - ..deleteText( - textNode, - selection.start.offset, - textNode.toRawString().length - selection.start.offset, - ) - ..afterSelection = afterSelection - ..commit(); + final transactionBuilder = TransactionBuilder(editorState); + transactionBuilder.insertNode( + textNode.path.next, + textNode.copyWith( + attributes: attributes, + delta: textNode.delta.slice(selection.end.offset), + ), + ); + transactionBuilder.deleteText( + textNode, + selection.start.offset, + textNode.toRawString().length - selection.start.offset, + ); + if (textNode.children.isNotEmpty) { + final children = textNode.children.toList(growable: false); + transactionBuilder.deleteNodes(children); + } + transactionBuilder.afterSelection = afterSelection; + transactionBuilder.commit(); // If the new type of a text node is number list, // the numbers of the following nodes should be incremental. diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/tab_handler.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/tab_handler.dart new file mode 100644 index 0000000000..0eb36fff17 --- /dev/null +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/tab_handler.dart @@ -0,0 +1,34 @@ +import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:flutter/material.dart'; + +ShortcutEventHandler tabHandler = (editorState, event) { + // Only Supports BulletedList For Now. + + final selection = editorState.service.selectionService.currentSelection.value; + final textNodes = editorState.service.selectionService.currentSelectedNodes + .whereType(); + if (textNodes.length != 1 || selection == null || !selection.isSingle) { + return KeyEventResult.ignored; + } + + final textNode = textNodes.first; + final previous = textNode.previous; + if (textNode.subtype != BuiltInAttributeKey.bulletedList || + previous == null || + previous.subtype != BuiltInAttributeKey.bulletedList) { + return KeyEventResult.handled; + } + + final path = previous.path + [previous.children.length]; + final afterSelection = Selection( + start: selection.start.copyWith(path: path), + end: selection.end.copyWith(path: path), + ); + TransactionBuilder(editorState) + ..deleteNode(textNode) + ..insertNode(path, textNode) + ..setAfterSelection(afterSelection) + ..commit(); + + return KeyEventResult.handled; +}; diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/keyboard_service.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/keyboard_service.dart index 790e9dd94f..5259872b95 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/keyboard_service.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/keyboard_service.dart @@ -42,6 +42,7 @@ abstract class AppFlowyKeyboardService { class AppFlowyKeyboard extends StatefulWidget { const AppFlowyKeyboard({ Key? key, + this.editable = true, required this.shortcutEvents, required this.editorState, required this.child, @@ -50,6 +51,7 @@ class AppFlowyKeyboard extends StatefulWidget { final EditorState editorState; final Widget child; final List shortcutEvents; + final bool editable; @override State createState() => _AppFlowyKeyboardState(); @@ -62,7 +64,6 @@ class _AppFlowyKeyboardState extends State bool isFocus = true; @override - // TODO: implement shortcutEvents List get shortcutEvents => widget.shortcutEvents; @override @@ -91,8 +92,12 @@ class _AppFlowyKeyboardState extends State @override void enable() { - isFocus = true; - _focusNode.requestFocus(); + if (widget.editable) { + isFocus = true; + _focusNode.requestFocus(); + } else { + disable(); + } } @override diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/selection_service.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/selection_service.dart index d755e8c9f9..d9b5422aa1 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/selection_service.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/selection_service.dart @@ -84,6 +84,7 @@ class AppFlowySelection extends StatefulWidget { Key? key, this.cursorColor = const Color(0xFF00BCF0), this.selectionColor = const Color.fromARGB(53, 111, 201, 231), + this.editable = true, required this.editorState, required this.child, }) : super(key: key); @@ -92,6 +93,7 @@ class AppFlowySelection extends StatefulWidget { final Widget child; final Color cursorColor; final Color selectionColor; + final bool editable; @override State createState() => _AppFlowySelectionState(); @@ -144,15 +146,21 @@ class _AppFlowySelectionState extends State @override Widget build(BuildContext context) { - return SelectionGestureDetector( - onPanStart: _onPanStart, - onPanUpdate: _onPanUpdate, - onPanEnd: _onPanEnd, - onTapDown: _onTapDown, - onDoubleTapDown: _onDoubleTapDown, - onTripleTapDown: _onTripleTapDown, - child: widget.child, - ); + if (!widget.editable) { + return Container( + child: widget.child, + ); + } else { + return SelectionGestureDetector( + onPanStart: _onPanStart, + onPanUpdate: _onPanUpdate, + onPanEnd: _onPanEnd, + onTapDown: _onTapDown, + onDoubleTapDown: _onDoubleTapDown, + onTripleTapDown: _onTripleTapDown, + child: widget.child, + ); + } } @override @@ -184,6 +192,10 @@ class _AppFlowySelectionState extends State @override void updateSelection(Selection? selection) { + if (!widget.editable) { + return; + } + selectionRects.clear(); clearSelection(); @@ -323,6 +335,7 @@ class _AppFlowySelectionState extends State // compute the selection in range. if (first != null && last != null) { + Log.selection.debug('first = $first, last = $last'); final start = first.getSelectionInRange(panStartOffset, panEndOffset).start; final end = last.getSelectionInRange(panStartOffset, panEndOffset).end; @@ -353,6 +366,8 @@ class _AppFlowySelectionState extends State final normalizedSelection = selection.normalize; assert(normalizedSelection.isBackward); + Log.selection.debug('update selection areas, $normalizedSelection'); + for (var i = 0; i < backwardNodes.length; i++) { final node = backwardNodes[i]; final selectable = node.selectable; diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/shortcut_event/built_in_shortcut_events.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/shortcut_event/built_in_shortcut_events.dart index eafee79a6d..38eb9ee7c5 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/shortcut_event/built_in_shortcut_events.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/shortcut_event/built_in_shortcut_events.dart @@ -9,6 +9,7 @@ import 'package:appflowy_editor/src/service/internal_key_event_handlers/redo_und import 'package:appflowy_editor/src/service/internal_key_event_handlers/select_all_handler.dart'; import 'package:appflowy_editor/src/service/internal_key_event_handlers/slash_handler.dart'; import 'package:appflowy_editor/src/service/internal_key_event_handlers/format_style_handler.dart'; +import 'package:appflowy_editor/src/service/internal_key_event_handlers/tab_handler.dart'; import 'package:appflowy_editor/src/service/internal_key_event_handlers/whitespace_handler.dart'; import 'package:appflowy_editor/src/service/shortcut_event/shortcut_event.dart'; @@ -243,4 +244,9 @@ List builtInShortcutEvents = [ command: 'page down', handler: pageDownHandler, ), + ShortcutEvent( + key: 'Tab', + command: 'tab', + handler: tabHandler, + ), ]; diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/shortcut_event/shortcut_event.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/shortcut_event/shortcut_event.dart index ae64b1635c..fb1a245b00 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/shortcut_event/shortcut_event.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/shortcut_event/shortcut_event.dart @@ -2,6 +2,7 @@ import 'dart:io'; import 'package:appflowy_editor/src/service/shortcut_event/keybinding.dart'; import 'package:appflowy_editor/src/service/shortcut_event/shortcut_event_handler.dart'; +import 'package:flutter/foundation.dart'; /// Defines the implementation of shortcut event. class ShortcutEvent { @@ -56,7 +57,10 @@ class ShortcutEvent { String? linuxCommand, }) { var matched = false; - if (Platform.isWindows && + if (kIsWeb && command != null && command.isNotEmpty) { + this.command = command; + matched = true; + } else if (Platform.isWindows && windowsCommand != null && windowsCommand.isNotEmpty) { this.command = windowsCommand; diff --git a/frontend/app_flowy/packages/appflowy_editor/test/document/node_test.dart b/frontend/app_flowy/packages/appflowy_editor/test/document/node_test.dart new file mode 100644 index 0000000000..3ff00eaa7a --- /dev/null +++ b/frontend/app_flowy/packages/appflowy_editor/test/document/node_test.dart @@ -0,0 +1,153 @@ +import 'dart:collection'; + +import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() async { + setUpAll(() { + TestWidgetsFlutterBinding.ensureInitialized(); + }); + + group('node.dart', () { + test('test node copyWith', () { + final node = Node( + type: 'example', + children: LinkedList(), + attributes: { + 'example': 'example', + }, + ); + expect(node.toJson(), { + 'type': 'example', + 'attributes': { + 'example': 'example', + }, + }); + expect( + node.copyWith().toJson(), + node.toJson(), + ); + + final nodeWithChildren = Node( + type: 'example', + children: LinkedList()..add(node), + attributes: { + 'example': 'example', + }, + ); + expect(nodeWithChildren.toJson(), { + 'type': 'example', + 'attributes': { + 'example': 'example', + }, + 'children': [ + { + 'type': 'example', + 'attributes': { + 'example': 'example', + }, + }, + ], + }); + expect( + nodeWithChildren.copyWith().toJson(), + nodeWithChildren.toJson(), + ); + }); + + test('test textNode copyWith', () { + final textNode = TextNode( + type: 'example', + children: LinkedList(), + attributes: { + 'example': 'example', + }, + delta: Delta()..insert('AppFlowy'), + ); + expect(textNode.toJson(), { + 'type': 'example', + 'attributes': { + 'example': 'example', + }, + 'delta': [ + {'insert': 'AppFlowy'}, + ], + }); + expect( + textNode.copyWith().toJson(), + textNode.toJson(), + ); + + final textNodeWithChildren = TextNode( + type: 'example', + children: LinkedList()..add(textNode), + attributes: { + 'example': 'example', + }, + delta: Delta()..insert('AppFlowy'), + ); + expect(textNodeWithChildren.toJson(), { + 'type': 'example', + 'attributes': { + 'example': 'example', + }, + 'delta': [ + {'insert': 'AppFlowy'}, + ], + 'children': [ + { + 'type': 'example', + 'attributes': { + 'example': 'example', + }, + 'delta': [ + {'insert': 'AppFlowy'}, + ], + }, + ], + }); + expect( + textNodeWithChildren.copyWith().toJson(), + textNodeWithChildren.toJson(), + ); + }); + + test('test node path', () { + Node previous = Node( + type: 'example', + attributes: {}, + children: LinkedList(), + ); + const len = 10; + for (var i = 0; i < len; i++) { + final node = Node( + type: 'example_$i', + attributes: {}, + children: LinkedList(), + ); + previous.children.add(node..parent = previous); + previous = node; + } + expect(previous.path, List.filled(len, 0)); + }); + + test('test copy with', () { + final child = Node( + type: 'child', + attributes: {}, + children: LinkedList(), + ); + final base = Node( + type: 'base', + attributes: {}, + children: LinkedList()..add(child), + ); + final node = base.copyWith( + type: 'node', + ); + expect(identical(node.attributes, base.attributes), false); + expect(identical(node.children, base.children), false); + expect(identical(node.children.first, base.children.first), false); + }); + }); +} diff --git a/frontend/app_flowy/packages/appflowy_editor/test/extensions/path_extensions_test.dart b/frontend/app_flowy/packages/appflowy_editor/test/extensions/path_extensions_test.dart new file mode 100644 index 0000000000..c39c0d0e56 --- /dev/null +++ b/frontend/app_flowy/packages/appflowy_editor/test/extensions/path_extensions_test.dart @@ -0,0 +1,38 @@ +import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:appflowy_editor/src/extensions/path_extensions.dart'; + +void main() async { + setUpAll(() { + TestWidgetsFlutterBinding.ensureInitialized(); + }); + + group('path_extensions.dart', () { + test('test path equality', () { + var p1 = [0, 0]; + var p2 = [0]; + + expect(p1 > p2, true); + expect(p1 >= p2, true); + expect(p1 < p2, false); + expect(p1 <= p2, false); + + p1 = [1, 1, 2]; + p2 = [1, 1, 3]; + + expect(p2 > p1, true); + expect(p2 >= p1, true); + expect(p2 < p1, false); + expect(p2 <= p1, false); + + p1 = [2, 0, 1]; + p2 = [2, 0, 1]; + + expect(p2 > p1, false); + expect(p1 > p2, false); + expect(p2 >= p1, true); + expect(p2 <= p1, true); + expect(pathEquals(p1, p2), true); + }); + }); +} diff --git a/frontend/app_flowy/packages/appflowy_editor/test/infra/test_editor.dart b/frontend/app_flowy/packages/appflowy_editor/test/infra/test_editor.dart index de69f95803..b4282a013e 100644 --- a/frontend/app_flowy/packages/appflowy_editor/test/infra/test_editor.dart +++ b/frontend/app_flowy/packages/appflowy_editor/test/infra/test_editor.dart @@ -19,6 +19,7 @@ class EditorWidgetTester { EditorState get editorState => _editorState; Node get root => _editorState.document.root; + StateTree get document => _editorState.document; int get documentLength => _editorState.document.root.children.length; Selection? get documentSelection => _editorState.service.selectionService.currentSelection.value; diff --git a/frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/backspace_handler_test.dart b/frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/backspace_handler_test.dart index 540dca6e31..f40e8a8fa5 100644 --- a/frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/backspace_handler_test.dart +++ b/frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/backspace_handler_test.dart @@ -4,7 +4,6 @@ import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:network_image_mock/network_image_mock.dart'; import '../../infra/test_editor.dart'; -import 'package:appflowy_editor/src/document/built_in_attribute_keys.dart'; void main() async { setUpAll(() { @@ -267,6 +266,60 @@ void main() async { BuiltInAttributeKey.h1, ); }); + + testWidgets('Delete the nested bulleted list', (tester) async { + // * Welcome to Appflowy 😁 + // * Welcome to Appflowy 😁 + // * Welcome to Appflowy 😁 + const text = 'Welcome to Appflowy 😁'; + final node = TextNode( + type: 'text', + delta: Delta()..insert(text), + attributes: { + BuiltInAttributeKey.subtype: BuiltInAttributeKey.bulletedList, + }, + ); + node.insert( + node.copyWith() + ..insert( + node.copyWith(), + ), + ); + + final editor = tester.editor..insert(node); + await editor.startTesting(); + + // * Welcome to Appflowy 😁 + // * Welcome to Appflowy 😁 + // Welcome to Appflowy 😁 + await editor.updateSelection( + Selection.single(path: [0, 0, 0], startOffset: 0), + ); + await editor.pressLogicKey(LogicalKeyboardKey.backspace); + expect(editor.nodeAtPath([0, 0, 0])?.subtype, null); + await editor.updateSelection( + Selection.single(path: [0, 0, 0], startOffset: 0), + ); + await editor.pressLogicKey(LogicalKeyboardKey.backspace); + expect(editor.nodeAtPath([0, 1]) != null, true); + await editor.updateSelection( + Selection.single(path: [0, 1], startOffset: 0), + ); + await editor.pressLogicKey(LogicalKeyboardKey.backspace); + expect(editor.nodeAtPath([1]) != null, true); + await editor.updateSelection( + Selection.single(path: [1], startOffset: 0), + ); + + // * Welcome to Appflowy 😁 + // * Welcome to Appflowy 😁Welcome to Appflowy 😁 + await editor.pressLogicKey(LogicalKeyboardKey.backspace); + expect( + editor.documentSelection, + Selection.single(path: [0, 0], startOffset: text.length), + ); + expect((editor.nodeAtPath([0, 0]) as TextNode).toRawString(), text * 2); + }); } Future _deleteFirstImage(WidgetTester tester, bool isBackward) async { diff --git a/frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/tab_handler_test.dart b/frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/tab_handler_test.dart new file mode 100644 index 0000000000..1374869deb --- /dev/null +++ b/frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/tab_handler_test.dart @@ -0,0 +1,151 @@ +import 'package:appflowy_editor/appflowy_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('tab_handler.dart', () { + testWidgets('press tab in plain text', (tester) async { + const text = 'Welcome to Appflowy 😁'; + final editor = tester.editor + ..insertTextNode(text) + ..insertTextNode(text); + await editor.startTesting(); + final document = editor.document; + + var selection = Selection.single(path: [0], startOffset: 0); + await editor.updateSelection(selection); + await editor.pressLogicKey(LogicalKeyboardKey.tab); + + // nothing happens + expect(editor.documentSelection, selection); + expect(editor.document.toJson(), document.toJson()); + + selection = Selection.single(path: [1], startOffset: 0); + await editor.updateSelection(selection); + await editor.pressLogicKey(LogicalKeyboardKey.tab); + + // nothing happens + expect(editor.documentSelection, selection); + expect(editor.document.toJson(), document.toJson()); + }); + + testWidgets('press tab in bulleted list', (tester) async { + const text = 'Welcome to Appflowy 😁'; + final editor = tester.editor + ..insertTextNode( + text, + attributes: { + BuiltInAttributeKey.subtype: BuiltInAttributeKey.bulletedList + }, + ) + ..insertTextNode( + text, + attributes: { + BuiltInAttributeKey.subtype: BuiltInAttributeKey.bulletedList + }, + ) + ..insertTextNode( + text, + attributes: { + BuiltInAttributeKey.subtype: BuiltInAttributeKey.bulletedList + }, + ); + await editor.startTesting(); + var document = editor.document; + + var selection = Selection.single(path: [0], startOffset: 0); + await editor.updateSelection(selection); + await editor.pressLogicKey(LogicalKeyboardKey.tab); + + // nothing happens + expect(editor.documentSelection, selection); + expect(editor.document.toJson(), document.toJson()); + + // Before + // * Welcome to Appflowy 😁 + // * Welcome to Appflowy 😁 + // * Welcome to Appflowy 😁 + // After + // * Welcome to Appflowy 😁 + // * Welcome to Appflowy 😁 + // * Welcome to Appflowy 😁 + + selection = Selection.single(path: [1], startOffset: 0); + await editor.updateSelection(selection); + + await editor.pressLogicKey(LogicalKeyboardKey.tab); + + expect( + editor.documentSelection, + Selection.single(path: [0, 0], startOffset: 0), + ); + expect(editor.nodeAtPath([0])!.subtype, BuiltInAttributeKey.bulletedList); + expect(editor.nodeAtPath([1])!.subtype, BuiltInAttributeKey.bulletedList); + expect(editor.nodeAtPath([2]), null); + expect( + editor.nodeAtPath([0, 0])!.subtype, BuiltInAttributeKey.bulletedList); + + selection = Selection.single(path: [1], startOffset: 0); + await editor.updateSelection(selection); + await editor.pressLogicKey(LogicalKeyboardKey.tab); + + expect( + editor.documentSelection, + Selection.single(path: [0, 1], startOffset: 0), + ); + expect(editor.nodeAtPath([0])!.subtype, BuiltInAttributeKey.bulletedList); + expect(editor.nodeAtPath([1]), null); + expect(editor.nodeAtPath([2]), null); + expect( + editor.nodeAtPath([0, 0])!.subtype, BuiltInAttributeKey.bulletedList); + expect( + editor.nodeAtPath([0, 1])!.subtype, BuiltInAttributeKey.bulletedList); + + // Before + // * Welcome to Appflowy 😁 + // * Welcome to Appflowy 😁 + // * Welcome to Appflowy 😁 + // After + // * Welcome to Appflowy 😁 + // * Welcome to Appflowy 😁 + // * Welcome to Appflowy 😁 + document = editor.document; + selection = Selection.single(path: [0, 0], startOffset: 0); + await editor.updateSelection(selection); + await editor.pressLogicKey(LogicalKeyboardKey.tab); + + expect( + editor.documentSelection, + Selection.single(path: [0, 0], startOffset: 0), + ); + expect(editor.document.toJson(), document.toJson()); + + selection = Selection.single(path: [0, 1], startOffset: 0); + await editor.updateSelection(selection); + await editor.pressLogicKey(LogicalKeyboardKey.tab); + + expect( + editor.documentSelection, + Selection.single(path: [0, 0, 0], startOffset: 0), + ); + expect( + editor.nodeAtPath([0])!.subtype, + BuiltInAttributeKey.bulletedList, + ); + expect( + editor.nodeAtPath([0, 0])!.subtype, + BuiltInAttributeKey.bulletedList, + ); + expect(editor.nodeAtPath([0, 1]), null); + expect( + editor.nodeAtPath([0, 0, 0])!.subtype, + BuiltInAttributeKey.bulletedList, + ); + }); + }); +} From ab353551d135c5693d48862fd3f57011f79dacbf Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Fri, 23 Sep 2022 22:59:56 +0800 Subject: [PATCH 15/25] feat: implement find the forward nearest text node --- .../lib/src/render/rich_text/rich_text.dart | 8 +- .../backspace_handler.dart | 52 +++++++ .../backspace_handler_test.dart | 131 ++++++++++++++++++ 3 files changed, 189 insertions(+), 2 deletions(-) diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/rich_text.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/rich_text.dart index a6bc269284..3d7f895fd9 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/rich_text.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/rich_text.dart @@ -43,7 +43,11 @@ class RichTextNodeWidget extends BuiltInTextWidget { // customize class _RichTextNodeWidgetState extends State - with SelectableMixin, DefaultSelectable, BuiltInStyleMixin { + with + SelectableMixin, + DefaultSelectable, + BuiltInStyleMixin, + BuiltInTextWidgetMixin { @override GlobalKey? get iconKey => null; @@ -59,7 +63,7 @@ class _RichTextNodeWidgetState extends State } @override - Widget build(BuildContext context) { + Widget buildWithSingle(BuildContext context) { return Padding( padding: padding, child: FlowyRichText( diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/backspace_handler.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/backspace_handler.dart index 3df482012f..fad5bb7337 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/backspace_handler.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/backspace_handler.dart @@ -288,3 +288,55 @@ Node? _closestTextNode(Node? node) { } return null; } + +TextNode? findLastTextNode(Node node) { + final children = node.children.toList(growable: false).reversed; + for (final child in children) { + if (child.children.isNotEmpty) { + final result = findLastTextNode(child); + if (result != null) { + return result; + } + } + if (child is TextNode) { + return child; + } + } + if (node is TextNode) { + return node; + } + return null; +} + +// find the forward nearest text node +TextNode? forwardNearestTextNode(Node node) { + var previous = node.previous; + while (previous != null) { + final lastTextNode = findLastTextNode(previous); + if (lastTextNode != null) { + return lastTextNode; + } + if (previous is TextNode) { + return previous; + } + previous = previous.previous; + } + final parent = node.parent; + if (parent != null) { + if (parent is TextNode) { + return parent; + } + return forwardNearestTextNode(parent); + } + return null; +} + +Node? _forwardNearestTextNode(Node node) { + if (node is TextNode) { + return node; + } + if (node.next != null) { + return _forwardNearestTextNode(node.next!); + } + return null; +} diff --git a/frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/backspace_handler_test.dart b/frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/backspace_handler_test.dart index f40e8a8fa5..246c6ecb27 100644 --- a/frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/backspace_handler_test.dart +++ b/frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/backspace_handler_test.dart @@ -1,5 +1,9 @@ +import 'dart:collection'; + import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy_editor/src/render/image/image_node_widget.dart'; +import 'package:appflowy_editor/src/render/rich_text/flowy_rich_text.dart'; +import 'package:appflowy_editor/src/service/internal_key_event_handlers/backspace_handler.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:network_image_mock/network_image_mock.dart'; @@ -320,6 +324,133 @@ void main() async { ); expect((editor.nodeAtPath([0, 0]) as TextNode).toRawString(), text * 2); }); + + testWidgets('Delete the complicated nested bulleted list', (tester) async { + // * Welcome to Appflowy 😁 + // * Welcome to Appflowy 😁 + // * Welcome to Appflowy 😁 + // * Welcome to Appflowy 😁 + // * Welcome to Appflowy 😁 + const text = 'Welcome to Appflowy 😁'; + final node = TextNode( + type: 'text', + delta: Delta()..insert(text), + attributes: { + BuiltInAttributeKey.subtype: BuiltInAttributeKey.bulletedList, + }, + ); + + node + ..insert( + node.copyWith(children: LinkedList()), + ) + ..insert( + node.copyWith(children: LinkedList()) + ..insert( + node.copyWith(children: LinkedList()), + ) + ..insert( + node.copyWith(children: LinkedList()), + ), + ); + + final editor = tester.editor..insert(node); + await editor.startTesting(); + + await editor.updateSelection( + Selection.single(path: [0, 1], startOffset: 0), + ); + await editor.pressLogicKey(LogicalKeyboardKey.backspace); + expect( + editor.nodeAtPath([0, 1])!.subtype != BuiltInAttributeKey.bulletedList, + true, + ); + expect( + editor.nodeAtPath([0, 1, 0])!.subtype, + BuiltInAttributeKey.bulletedList, + ); + expect( + editor.nodeAtPath([0, 1, 1])!.subtype, + BuiltInAttributeKey.bulletedList, + ); + expect(find.byType(FlowyRichText), findsNWidgets(5)); + + // Before + // * Welcome to Appflowy 😁 + // * Welcome to Appflowy 😁 + // Welcome to Appflowy 😁 + // * Welcome to Appflowy 😁 + // * Welcome to Appflowy 😁 + // After + // * Welcome to Appflowy 😁 + // * Welcome to Appflowy 😁 + // Welcome to Appflowy 😁 + // * Welcome to Appflowy 😁 + // * Welcome to Appflowy 😁 + await editor.pressLogicKey(LogicalKeyboardKey.backspace); + expect( + editor.nodeAtPath([1])!.subtype != BuiltInAttributeKey.bulletedList, + true, + ); + expect( + editor.nodeAtPath([1, 0])!.subtype == BuiltInAttributeKey.bulletedList, + true, + ); + expect( + editor.nodeAtPath([1, 1])!.subtype == BuiltInAttributeKey.bulletedList, + true, + ); + + // After + // * Welcome to Appflowy 😁 + // * Welcome to Appflowy 😁Welcome to Appflowy 😁 + // * Welcome to Appflowy 😁 + // * Welcome to Appflowy 😁 + }); + + test('find the last text node', () { + // * Welcome to Appflowy 😁 + // * Welcome to Appflowy 😁 + // * Welcome to Appflowy 😁 + // * Welcome to Appflowy 😁 + // * Welcome to Appflowy 😁 + // * Welcome to Appflowy 😁 + // * Welcome to Appflowy 😁 + const text = 'Welcome to Appflowy 😁'; + TextNode textNode() { + return TextNode( + type: 'text', + delta: Delta()..insert(text), + ); + } + + final node110 = textNode(); + final node111 = textNode(); + final node11 = textNode() + ..insert(node110) + ..insert(node111); + final node10 = textNode(); + final node1 = textNode() + ..insert(node10) + ..insert(node11); + final node0 = textNode(); + final node = textNode() + ..insert(node0) + ..insert(node1); + + expect(findLastTextNode(node)?.path, [1, 1, 1]); + expect(findLastTextNode(node0)?.path, [0]); + expect(findLastTextNode(node1)?.path, [1, 1, 1]); + expect(findLastTextNode(node10)?.path, [1, 0]); + expect(findLastTextNode(node11)?.path, [1, 1, 1]); + + expect(forwardNearestTextNode(node111)?.path, [1, 1, 0]); + expect(forwardNearestTextNode(node110)?.path, [1, 1]); + expect(forwardNearestTextNode(node11)?.path, [1, 0]); + expect(forwardNearestTextNode(node10)?.path, [1]); + expect(forwardNearestTextNode(node1)?.path, [0]); + expect(forwardNearestTextNode(node0)?.path, []); + }); } Future _deleteFirstImage(WidgetTester tester, bool isBackward) async { From d648f2b5b924583acc1bd1de300f67db6370e5a2 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Fri, 23 Sep 2022 23:08:48 +0800 Subject: [PATCH 16/25] fix: merge current text node's children to previous text node --- .../backspace_handler.dart | 74 +++++++------------ 1 file changed, 25 insertions(+), 49 deletions(-) diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/backspace_handler.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/backspace_handler.dart index fad5bb7337..c9504918af 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/backspace_handler.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/backspace_handler.dart @@ -2,7 +2,6 @@ import 'package:appflowy_editor/src/service/internal_key_event_handlers/number_l import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:appflowy_editor/src/extensions/path_extensions.dart'; // Handle delete text. ShortcutEventHandler deleteTextHandler = (editorState, event) { @@ -128,33 +127,38 @@ KeyEventResult _backDeleteToPreviousTextNode( Selection selection, ) { // Not reach to the root. - if (textNode.parent?.parent != null) { - transactionBuilder - ..deleteNode(textNode) - ..insertNode(textNode.parent!.path.next, textNode) - ..afterSelection = Selection.collapsed( - Position(path: textNode.parent!.path.next, offset: 0), - ) - ..commit(); - return KeyEventResult.handled; - } + // if (textNode.parent?.parent != null) { + // transactionBuilder + // ..deleteNode(textNode) + // ..insertNode(textNode.parent!.path.next, textNode) + // ..afterSelection = Selection.collapsed( + // Position(path: textNode.parent!.path.next, offset: 0), + // ) + // ..commit(); + // return KeyEventResult.handled; + // } bool prevIsNumberList = false; - final previousTextNode = _closestTextNode(textNode.previous); - if (previousTextNode != null && previousTextNode is TextNode) { + final previousTextNode = forwardNearestTextNode(textNode); + if (previousTextNode != null) { if (previousTextNode.subtype == BuiltInAttributeKey.numberList) { prevIsNumberList = true; } - transactionBuilder - ..mergeText(previousTextNode, textNode) - ..deleteNode(textNode) - ..afterSelection = Selection.collapsed( - Position( - path: previousTextNode.path, - offset: previousTextNode.toRawString().length, - ), + transactionBuilder.mergeText(previousTextNode, textNode); + transactionBuilder.deleteNode(textNode); + if (textNode.children.isNotEmpty) { + transactionBuilder.insertNodes( + previousTextNode.path + [0], + textNode.children.toList(growable: false), ); + } + transactionBuilder.afterSelection = Selection.collapsed( + Position( + path: previousTextNode.path, + offset: previousTextNode.toRawString().length, + ), + ); } if (transactionBuilder.operations.isNotEmpty) { @@ -271,24 +275,6 @@ void _deleteTextNodes(TransactionBuilder transactionBuilder, } // TODO: Just a simple solution for textNode, need to be optimized. -Node? _closestTextNode(Node? node) { - if (node is TextNode) { - var children = node.children; - if (children.isEmpty) { - return node; - } - var last = children.last; - while (last.children.isNotEmpty) { - last = children.last; - } - return last; - } - if (node?.previous != null) { - return _closestTextNode(node!.previous!); - } - return null; -} - TextNode? findLastTextNode(Node node) { final children = node.children.toList(growable: false).reversed; for (final child in children) { @@ -330,13 +316,3 @@ TextNode? forwardNearestTextNode(Node node) { } return null; } - -Node? _forwardNearestTextNode(Node node) { - if (node is TextNode) { - return node; - } - if (node.next != null) { - return _forwardNearestTextNode(node.next!); - } - return null; -} From 91469df1262d2faa81347fa1939d23e5125873a1 Mon Sep 17 00:00:00 2001 From: weidong fu Date: Sat, 24 Sep 2022 10:19:37 +0800 Subject: [PATCH 17/25] chore: fix typo in install_macos.sh --- frontend/scripts/install_dev_env/install_macos.sh | 2 -- 1 file changed, 2 deletions(-) diff --git a/frontend/scripts/install_dev_env/install_macos.sh b/frontend/scripts/install_dev_env/install_macos.sh index f4ae69390c..f7e9d73909 100755 --- a/frontend/scripts/install_dev_env/install_macos.sh +++ b/frontend/scripts/install_dev_env/install_macos.sh @@ -34,8 +34,6 @@ else printMessage "Skipping Rust installation." fi -abvc - # Install sqllite printMessage "Installing sqlLite3." brew install sqlite3 From 6bda1fd2ead4a89e1b66b010df58560e55e64c0b Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Sat, 24 Sep 2022 12:01:23 +0800 Subject: [PATCH 18/25] fix: delete the nested bulleted list will lost the children nodes --- .../appflowy_editor/lib/src/infra/infra.dart | 46 ++++++++++++ .../backspace_handler.dart | 74 +++++-------------- .../test/infra/infra_test.dart | 51 +++++++++++++ .../backspace_handler_test.dart | 66 +++-------------- 4 files changed, 123 insertions(+), 114 deletions(-) create mode 100644 frontend/app_flowy/packages/appflowy_editor/lib/src/infra/infra.dart create mode 100644 frontend/app_flowy/packages/appflowy_editor/test/infra/infra_test.dart diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/infra/infra.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/infra/infra.dart new file mode 100644 index 0000000000..346e9331f5 --- /dev/null +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/infra/infra.dart @@ -0,0 +1,46 @@ +import 'package:appflowy_editor/src/document/node.dart'; + +class Infra { +// find the forward nearest text node + static TextNode? forwardNearestTextNode(Node node) { + var previous = node.previous; + while (previous != null) { + final lastTextNode = findLastTextNode(previous); + if (lastTextNode != null) { + return lastTextNode; + } + if (previous is TextNode) { + return previous; + } + previous = previous.previous; + } + final parent = node.parent; + if (parent != null) { + if (parent is TextNode) { + return parent; + } + return forwardNearestTextNode(parent); + } + return null; + } + + // find the last text node + static TextNode? findLastTextNode(Node node) { + final children = node.children.toList(growable: false).reversed; + for (final child in children) { + if (child.children.isNotEmpty) { + final result = findLastTextNode(child); + if (result != null) { + return result; + } + } + if (child is TextNode) { + return child; + } + } + if (node is TextNode) { + return node; + } + return null; + } +} diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/backspace_handler.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/backspace_handler.dart index c9504918af..3f7a54810f 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/backspace_handler.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/backspace_handler.dart @@ -1,7 +1,9 @@ +import 'package:appflowy_editor/src/infra/infra.dart'; import 'package:appflowy_editor/src/service/internal_key_event_handlers/number_list_helper.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:appflowy_editor/src/extensions/path_extensions.dart'; // Handle delete text. ShortcutEventHandler deleteTextHandler = (editorState, event) { @@ -126,33 +128,34 @@ KeyEventResult _backDeleteToPreviousTextNode( List nonTextNodes, Selection selection, ) { - // Not reach to the root. - // if (textNode.parent?.parent != null) { - // transactionBuilder - // ..deleteNode(textNode) - // ..insertNode(textNode.parent!.path.next, textNode) - // ..afterSelection = Selection.collapsed( - // Position(path: textNode.parent!.path.next, offset: 0), - // ) - // ..commit(); - // return KeyEventResult.handled; - // } + if (textNode.next == null && + textNode.children.isEmpty && + textNode.parent?.parent != null) { + transactionBuilder + ..deleteNode(textNode) + ..insertNode(textNode.parent!.path.next, textNode) + ..afterSelection = Selection.collapsed( + Position(path: textNode.parent!.path.next, offset: 0), + ) + ..commit(); + return KeyEventResult.handled; + } bool prevIsNumberList = false; - final previousTextNode = forwardNearestTextNode(textNode); + final previousTextNode = Infra.forwardNearestTextNode(textNode); if (previousTextNode != null) { if (previousTextNode.subtype == BuiltInAttributeKey.numberList) { prevIsNumberList = true; } transactionBuilder.mergeText(previousTextNode, textNode); - transactionBuilder.deleteNode(textNode); if (textNode.children.isNotEmpty) { transactionBuilder.insertNodes( - previousTextNode.path + [0], + previousTextNode.path.next, textNode.children.toList(growable: false), ); } + transactionBuilder.deleteNode(textNode); transactionBuilder.afterSelection = Selection.collapsed( Position( path: previousTextNode.path, @@ -273,46 +276,3 @@ void _deleteTextNodes(TransactionBuilder transactionBuilder, secondOffset: selection.end.offset, ); } - -// TODO: Just a simple solution for textNode, need to be optimized. -TextNode? findLastTextNode(Node node) { - final children = node.children.toList(growable: false).reversed; - for (final child in children) { - if (child.children.isNotEmpty) { - final result = findLastTextNode(child); - if (result != null) { - return result; - } - } - if (child is TextNode) { - return child; - } - } - if (node is TextNode) { - return node; - } - return null; -} - -// find the forward nearest text node -TextNode? forwardNearestTextNode(Node node) { - var previous = node.previous; - while (previous != null) { - final lastTextNode = findLastTextNode(previous); - if (lastTextNode != null) { - return lastTextNode; - } - if (previous is TextNode) { - return previous; - } - previous = previous.previous; - } - final parent = node.parent; - if (parent != null) { - if (parent is TextNode) { - return parent; - } - return forwardNearestTextNode(parent); - } - return null; -} diff --git a/frontend/app_flowy/packages/appflowy_editor/test/infra/infra_test.dart b/frontend/app_flowy/packages/appflowy_editor/test/infra/infra_test.dart new file mode 100644 index 0000000000..e03d524d76 --- /dev/null +++ b/frontend/app_flowy/packages/appflowy_editor/test/infra/infra_test.dart @@ -0,0 +1,51 @@ +import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:appflowy_editor/src/infra/infra.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() async { + group('infra.dart', () { + test('find the last text node', () { + // * Welcome to Appflowy 😁 + // * Welcome to Appflowy 😁 + // * Welcome to Appflowy 😁 + // * Welcome to Appflowy 😁 + // * Welcome to Appflowy 😁 + // * Welcome to Appflowy 😁 + // * Welcome to Appflowy 😁 + const text = 'Welcome to Appflowy 😁'; + TextNode textNode() { + return TextNode( + type: 'text', + delta: Delta()..insert(text), + ); + } + + final node110 = textNode(); + final node111 = textNode(); + final node11 = textNode() + ..insert(node110) + ..insert(node111); + final node10 = textNode(); + final node1 = textNode() + ..insert(node10) + ..insert(node11); + final node0 = textNode(); + final node = textNode() + ..insert(node0) + ..insert(node1); + + expect(Infra.findLastTextNode(node)?.path, [1, 1, 1]); + expect(Infra.findLastTextNode(node0)?.path, [0]); + expect(Infra.findLastTextNode(node1)?.path, [1, 1, 1]); + expect(Infra.findLastTextNode(node10)?.path, [1, 0]); + expect(Infra.findLastTextNode(node11)?.path, [1, 1, 1]); + + expect(Infra.forwardNearestTextNode(node111)?.path, [1, 1, 0]); + expect(Infra.forwardNearestTextNode(node110)?.path, [1, 1]); + expect(Infra.forwardNearestTextNode(node11)?.path, [1, 0]); + expect(Infra.forwardNearestTextNode(node10)?.path, [1]); + expect(Infra.forwardNearestTextNode(node1)?.path, [0]); + expect(Infra.forwardNearestTextNode(node0)?.path, []); + }); + }); +} diff --git a/frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/backspace_handler_test.dart b/frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/backspace_handler_test.dart index 246c6ecb27..37ad7bade3 100644 --- a/frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/backspace_handler_test.dart +++ b/frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/backspace_handler_test.dart @@ -3,7 +3,6 @@ import 'dart:collection'; import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy_editor/src/render/image/image_node_widget.dart'; import 'package:appflowy_editor/src/render/rich_text/flowy_rich_text.dart'; -import 'package:appflowy_editor/src/service/internal_key_event_handlers/backspace_handler.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:network_image_mock/network_image_mock.dart'; @@ -383,73 +382,26 @@ void main() async { // * Welcome to Appflowy 😁 // After // * Welcome to Appflowy 😁 + // * Welcome to Appflowy 😁Welcome to Appflowy 😁 + // * Welcome to Appflowy 😁 // * Welcome to Appflowy 😁 - // Welcome to Appflowy 😁 - // * Welcome to Appflowy 😁 - // * Welcome to Appflowy 😁 await editor.pressLogicKey(LogicalKeyboardKey.backspace); expect( - editor.nodeAtPath([1])!.subtype != BuiltInAttributeKey.bulletedList, + editor.nodeAtPath([0, 0])!.subtype == BuiltInAttributeKey.bulletedList, true, ); expect( - editor.nodeAtPath([1, 0])!.subtype == BuiltInAttributeKey.bulletedList, + (editor.nodeAtPath([0, 0]) as TextNode).toRawString() == text * 2, true, ); expect( - editor.nodeAtPath([1, 1])!.subtype == BuiltInAttributeKey.bulletedList, + editor.nodeAtPath([0, 1])!.subtype == BuiltInAttributeKey.bulletedList, + true, + ); + expect( + editor.nodeAtPath([0, 2])!.subtype == BuiltInAttributeKey.bulletedList, true, ); - - // After - // * Welcome to Appflowy 😁 - // * Welcome to Appflowy 😁Welcome to Appflowy 😁 - // * Welcome to Appflowy 😁 - // * Welcome to Appflowy 😁 - }); - - test('find the last text node', () { - // * Welcome to Appflowy 😁 - // * Welcome to Appflowy 😁 - // * Welcome to Appflowy 😁 - // * Welcome to Appflowy 😁 - // * Welcome to Appflowy 😁 - // * Welcome to Appflowy 😁 - // * Welcome to Appflowy 😁 - const text = 'Welcome to Appflowy 😁'; - TextNode textNode() { - return TextNode( - type: 'text', - delta: Delta()..insert(text), - ); - } - - final node110 = textNode(); - final node111 = textNode(); - final node11 = textNode() - ..insert(node110) - ..insert(node111); - final node10 = textNode(); - final node1 = textNode() - ..insert(node10) - ..insert(node11); - final node0 = textNode(); - final node = textNode() - ..insert(node0) - ..insert(node1); - - expect(findLastTextNode(node)?.path, [1, 1, 1]); - expect(findLastTextNode(node0)?.path, [0]); - expect(findLastTextNode(node1)?.path, [1, 1, 1]); - expect(findLastTextNode(node10)?.path, [1, 0]); - expect(findLastTextNode(node11)?.path, [1, 1, 1]); - - expect(forwardNearestTextNode(node111)?.path, [1, 1, 0]); - expect(forwardNearestTextNode(node110)?.path, [1, 1]); - expect(forwardNearestTextNode(node11)?.path, [1, 0]); - expect(forwardNearestTextNode(node10)?.path, [1]); - expect(forwardNearestTextNode(node1)?.path, [0]); - expect(forwardNearestTextNode(node0)?.path, []); }); } From fa0a485c85c3c2da565dfbbd6aeda1d3c3024f7e Mon Sep 17 00:00:00 2001 From: appflowy Date: Sat, 24 Sep 2022 15:57:40 +0800 Subject: [PATCH 19/25] fix: move checkbox card fail --- .../flowy-grid/src/dart_notification.rs | 4 -- .../src/services/cell/any_cell_data.rs | 5 +- .../src/services/cell/cell_operation.rs | 35 +++++++++----- .../flowy-grid/src/services/group/action.rs | 1 + .../src/services/group/controller.rs | 22 +++++---- .../controller_impls/checkbox_controller.rs | 2 +- .../single_select_controller.rs | 2 +- .../select_option_controller/util.rs | 48 +++++++++++++++---- 8 files changed, 81 insertions(+), 38 deletions(-) diff --git a/frontend/rust-lib/flowy-grid/src/dart_notification.rs b/frontend/rust-lib/flowy-grid/src/dart_notification.rs index 8d7ec2ba36..7489682619 100644 --- a/frontend/rust-lib/flowy-grid/src/dart_notification.rs +++ b/frontend/rust-lib/flowy-grid/src/dart_notification.rs @@ -34,7 +34,3 @@ pub fn send_dart_notification(id: &str, ty: GridNotification) -> DartNotifyBuild DartNotifyBuilder::new(id, ty, OBSERVABLE_CATEGORY) } -#[tracing::instrument(level = "trace")] -pub fn send_anonymous_dart_notification(ty: GridNotification) -> DartNotifyBuilder { - DartNotifyBuilder::new("", ty, OBSERVABLE_CATEGORY) -} 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 8f76ea62c5..f7ef205435 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 @@ -19,7 +19,10 @@ impl std::str::FromStr for AnyCellData { type Err = FlowyError; fn from_str(s: &str) -> Result { - let type_option_cell_data: AnyCellData = serde_json::from_str(s)?; + let type_option_cell_data: AnyCellData = serde_json::from_str(s).map_err(|err| { + let msg = format!("Deserialize {} to any cell data failed. Serde error: {}", s, err); + FlowyError::internal().context(msg) + })?; Ok(type_option_cell_data) } } 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 20dd09a5ef..569a5f2f7d 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 @@ -1,6 +1,7 @@ use crate::entities::FieldType; use crate::services::cell::{AnyCellData, CellBytes}; use crate::services::field::*; +use std::fmt::Debug; use flowy_error::{ErrorCode, FlowyError, FlowyResult}; use flowy_grid_data_model::revision::{CellRevision, FieldRevision, FieldTypeRevision}; @@ -73,20 +74,30 @@ pub fn apply_cell_data_changeset>( Ok(AnyCellData::new(s, field_type).json()) } -pub fn decode_any_cell_data>(data: T, field_rev: &FieldRevision) -> CellBytes { - if let Ok(any_cell_data) = data.try_into() { - let AnyCellData { data, field_type } = any_cell_data; - let to_field_type = field_rev.ty.into(); - match try_decode_cell_data(data.into(), field_rev, &field_type, &to_field_type) { - Ok(cell_bytes) => cell_bytes, - Err(e) => { - tracing::error!("Decode cell data failed, {:?}", e); - CellBytes::default() +pub fn decode_any_cell_data + Debug>( + data: T, + field_rev: &FieldRevision, +) -> CellBytes { + match data.try_into() { + Ok(any_cell_data) => { + let AnyCellData { data, field_type } = any_cell_data; + let to_field_type = field_rev.ty.into(); + match try_decode_cell_data(data.into(), field_rev, &field_type, &to_field_type) { + Ok(cell_bytes) => cell_bytes, + Err(e) => { + tracing::error!("Decode cell data failed, {:?}", e); + CellBytes::default() + } } } - } else { - tracing::error!("Decode type option data failed"); - CellBytes::default() + Err(err) => { + tracing::error!( + "Decode type option data to type: {} failed: {:?}", + std::any::type_name::(), + err, + ); + CellBytes::default() + } } } diff --git a/frontend/rust-lib/flowy-grid/src/services/group/action.rs b/frontend/rust-lib/flowy-grid/src/services/group/action.rs index fc92b68c46..fd9adcb8b5 100644 --- a/frontend/rust-lib/flowy-grid/src/services/group/action.rs +++ b/frontend/rust-lib/flowy-grid/src/services/group/action.rs @@ -14,5 +14,6 @@ pub trait GroupAction: Send + Sync { fn can_group(&self, content: &str, cell_data: &Self::CellDataType) -> bool; fn add_row_if_match(&mut self, row_rev: &RowRevision, cell_data: &Self::CellDataType) -> Vec; fn remove_row_if_match(&mut self, row_rev: &RowRevision, cell_data: &Self::CellDataType) -> Vec; + // Move row from one group to another fn move_row(&mut self, cell_data: &Self::CellDataType, context: MoveGroupRowContext) -> Vec; } diff --git a/frontend/rust-lib/flowy-grid/src/services/group/controller.rs b/frontend/rust-lib/flowy-grid/src/services/group/controller.rs index 5ea8639c92..7a1d7dbe62 100644 --- a/frontend/rust-lib/flowy-grid/src/services/group/controller.rs +++ b/frontend/rust-lib/flowy-grid/src/services/group/controller.rs @@ -1,12 +1,10 @@ -use crate::entities::{GroupChangesetPB, GroupViewChangesetPB, InsertedRowPB, RowPB}; -use crate::services::cell::{decode_any_cell_data, CellBytesParser}; +use crate::entities::{ GroupChangesetPB, GroupViewChangesetPB, InsertedRowPB, RowPB}; +use crate::services::cell::{decode_any_cell_data, CellBytesParser,}; use crate::services::group::action::GroupAction; use crate::services::group::configuration::GroupContext; use crate::services::group::entities::Group; use flowy_error::FlowyResult; -use flowy_grid_data_model::revision::{ - FieldRevision, GroupConfigurationContentSerde, GroupRevision, RowChangeset, RowRevision, TypeOptionDataDeserializer, -}; +use flowy_grid_data_model::revision::{ FieldRevision, GroupConfigurationContentSerde, GroupRevision, RowChangeset, RowRevision, TypeOptionDataDeserializer}; use std::marker::PhantomData; use std::sync::Arc; @@ -86,8 +84,7 @@ where G: GroupGenerator, TypeOptionType = T>, { pub async fn new(field_rev: &Arc, mut configuration: GroupContext) -> FlowyResult { - let field_type_rev = field_rev.ty; - let type_option = field_rev.get_type_option::(field_type_rev); + let type_option = field_rev.get_type_option::(field_rev.ty); let groups = G::generate_groups(&field_rev.id, &configuration, &type_option); let _ = configuration.init_groups(groups, true)?; @@ -278,12 +275,19 @@ where } } + #[tracing::instrument(level = "trace", skip_all, err)] fn move_group_row(&mut self, context: MoveGroupRowContext) -> FlowyResult> { - if let Some(cell_rev) = context.row_rev.cells.get(&self.field_id) { - let cell_bytes = decode_any_cell_data(cell_rev.data.clone(), context.field_rev); + let cell_rev = match context.row_rev.cells.get(&self.field_id) { + Some(cell_rev) => Some(cell_rev.clone()), + None => self.default_cell_rev(), + }; + + if let Some(cell_rev) = cell_rev { + let cell_bytes = decode_any_cell_data(cell_rev.data, context.field_rev); let cell_data = cell_bytes.parser::

()?; Ok(self.move_row(&cell_data, context)) } else { + tracing::warn!("Unexpected moving group row, changes should not be empty"); Ok(vec![]) } } diff --git a/frontend/rust-lib/flowy-grid/src/services/group/controller_impls/checkbox_controller.rs b/frontend/rust-lib/flowy-grid/src/services/group/controller_impls/checkbox_controller.rs index a15b0cc62b..aa0a0bec96 100644 --- a/frontend/rust-lib/flowy-grid/src/services/group/controller_impls/checkbox_controller.rs +++ b/frontend/rust-lib/flowy-grid/src/services/group/controller_impls/checkbox_controller.rs @@ -80,7 +80,7 @@ impl GroupAction for CheckboxGroupController { fn move_row(&mut self, _cell_data: &Self::CellDataType, mut context: MoveGroupRowContext) -> Vec { let mut group_changeset = vec![]; self.group_ctx.iter_mut_all_groups(|group| { - if let Some(changeset) = move_group_row(group, &mut context) { + if let Some(changeset) = move_group_row(group, &mut context) { group_changeset.push(changeset); } }); diff --git a/frontend/rust-lib/flowy-grid/src/services/group/controller_impls/select_option_controller/single_select_controller.rs b/frontend/rust-lib/flowy-grid/src/services/group/controller_impls/select_option_controller/single_select_controller.rs index 0c86166520..8ba632cc81 100644 --- a/frontend/rust-lib/flowy-grid/src/services/group/controller_impls/select_option_controller/single_select_controller.rs +++ b/frontend/rust-lib/flowy-grid/src/services/group/controller_impls/select_option_controller/single_select_controller.rs @@ -49,7 +49,7 @@ impl GroupAction for SingleSelectGroupController { fn move_row(&mut self, _cell_data: &Self::CellDataType, mut context: MoveGroupRowContext) -> Vec { let mut group_changeset = vec![]; self.group_ctx.iter_mut_all_groups(|group| { - if let Some(changeset) = move_group_row(group, &mut context) { + if let Some(changeset) = move_group_row(group, &mut context) { group_changeset.push(changeset); } }); diff --git a/frontend/rust-lib/flowy-grid/src/services/group/controller_impls/select_option_controller/util.rs b/frontend/rust-lib/flowy-grid/src/services/group/controller_impls/select_option_controller/util.rs index a1d81b0d5c..9c3e71bc3e 100644 --- a/frontend/rust-lib/flowy-grid/src/services/group/controller_impls/select_option_controller/util.rs +++ b/frontend/rust-lib/flowy-grid/src/services/group/controller_impls/select_option_controller/util.rs @@ -1,11 +1,12 @@ -use crate::entities::{GroupChangesetPB, InsertedRowPB, RowPB}; -use crate::services::cell::insert_select_option_cell; -use crate::services::field::{SelectOptionCellDataPB, SelectOptionPB}; +use crate::entities::{FieldType, GroupChangesetPB, InsertedRowPB, RowPB}; +use crate::services::cell::{insert_checkbox_cell, insert_select_option_cell}; +use crate::services::field::{SelectOptionCellDataPB, SelectOptionPB, CHECK}; use crate::services::group::configuration::GroupContext; -use crate::services::group::{GeneratedGroup, Group}; - use crate::services::group::controller::MoveGroupRowContext; -use flowy_grid_data_model::revision::{GroupRevision, RowRevision, SelectOptionGroupConfigurationRevision}; +use crate::services::group::{GeneratedGroup, Group}; +use flowy_grid_data_model::revision::{ + CellRevision, FieldRevision, GroupRevision, RowRevision, SelectOptionGroupConfigurationRevision, +}; pub type SelectOptionGroupContext = GroupContext; @@ -109,10 +110,16 @@ pub fn move_group_row(group: &mut Group, context: &mut MoveGroupRowContext) -> O // Update the corresponding row's cell content. if from_index.is_none() { - tracing::debug!("Mark row:{} belong to group:{}", row_rev.id, group.id); - let cell_rev = insert_select_option_cell(group.id.clone(), field_rev); - row_changeset.cell_by_field_id.insert(field_rev.id.clone(), cell_rev); - changeset.updated_rows.push(RowPB::from(*row_rev)); + let cell_rev = make_inserted_cell_rev(&group.id, field_rev); + if let Some(cell_rev) = cell_rev { + tracing::debug!( + "Update content of the cell in the row:{} to group:{}", + row_rev.id, + group.id + ); + row_changeset.cell_by_field_id.insert(field_rev.id.clone(), cell_rev); + changeset.updated_rows.push(RowPB::from(*row_rev)); + } } } if changeset.is_empty() { @@ -122,6 +129,27 @@ pub fn move_group_row(group: &mut Group, context: &mut MoveGroupRowContext) -> O } } +pub fn make_inserted_cell_rev(group_id: &str, field_rev: &FieldRevision) -> Option { + let field_type: FieldType = field_rev.ty.into(); + match field_type { + FieldType::SingleSelect => { + let cell_rev = insert_select_option_cell(group_id.to_owned(), field_rev); + Some(cell_rev) + } + FieldType::MultiSelect => { + let cell_rev = insert_select_option_cell(group_id.to_owned(), field_rev); + Some(cell_rev) + } + FieldType::Checkbox => { + let cell_rev = insert_checkbox_cell(group_id == CHECK, field_rev); + Some(cell_rev) + } + _ => { + tracing::warn!("Unknown field type: {:?}", field_type); + None + } + } +} pub fn generate_select_option_groups( _field_id: &str, _group_ctx: &SelectOptionGroupContext, From e66b3b07dbd99ae3e9ce2ab26bc709beeb0b4b61 Mon Sep 17 00:00:00 2001 From: nathan Date: Sat, 24 Sep 2022 20:34:31 +0800 Subject: [PATCH 20/25] fix: duplicate board --- frontend/rust-lib/flowy-folder/src/manager.rs | 2 +- .../src/services/view/controller.rs | 6 ++++-- .../flowy-grid/src/dart_notification.rs | 1 - frontend/rust-lib/flowy-grid/src/manager.rs | 17 ++++++++++++++--- .../flowy-grid/src/services/grid_editor.rs | 2 ++ .../flowy-grid/src/services/grid_view_editor.rs | 5 +++++ .../src/services/grid_view_manager.rs | 6 ++++++ .../flowy-grid/src/services/group/controller.rs | 8 +++++--- .../controller_impls/checkbox_controller.rs | 2 +- .../single_select_controller.rs | 2 +- .../flowy-sdk/src/deps_resolve/folder_deps.rs | 4 ++-- .../src/revision/grid_rev.rs | 13 ++++++++++--- .../src/revision/grid_view.rs | 4 ++++ 13 files changed, 55 insertions(+), 17 deletions(-) diff --git a/frontend/rust-lib/flowy-folder/src/manager.rs b/frontend/rust-lib/flowy-folder/src/manager.rs index a8ed3d56c1..3a62924e83 100644 --- a/frontend/rust-lib/flowy-folder/src/manager.rs +++ b/frontend/rust-lib/flowy-folder/src/manager.rs @@ -260,7 +260,7 @@ pub trait ViewDataProcessor { fn close_container(&self, view_id: &str) -> FutureResult<(), FlowyError>; - fn get_delta_data(&self, view_id: &str) -> FutureResult; + fn get_view_data(&self, view_id: &str) -> FutureResult; fn create_default_view( &self, 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 1f0b61ebb1..402a68813b 100644 --- a/frontend/rust-lib/flowy-folder/src/services/view/controller.rs +++ b/frontend/rust-lib/flowy-folder/src/services/view/controller.rs @@ -61,11 +61,13 @@ impl ViewController { let processor = self.get_data_processor(params.data_type.clone())?; let user_id = self.user.user_id()?; if params.view_content_data.is_empty() { + tracing::trace!("Create view with build-in data"); let view_data = processor .create_default_view(&user_id, ¶ms.view_id, params.layout.clone()) .await?; params.view_content_data = view_data.to_vec(); } else { + tracing::trace!("Create view with view data"); let delta_data = processor .create_view_from_delta_data( &user_id, @@ -231,7 +233,7 @@ impl ViewController { .await?; let processor = self.get_data_processor(view_rev.data_type.clone())?; - let delta_bytes = processor.get_delta_data(view_id).await?; + let view_data = processor.get_view_data(view_id).await?; let duplicate_params = CreateViewParams { belong_to_id: view_rev.app_id.clone(), name: format!("{} (copy)", &view_rev.name), @@ -239,7 +241,7 @@ impl ViewController { thumbnail: view_rev.thumbnail, data_type: view_rev.data_type.into(), layout: view_rev.layout.into(), - view_content_data: delta_bytes.to_vec(), + view_content_data: view_data.to_vec(), view_id: gen_view_id(), }; diff --git a/frontend/rust-lib/flowy-grid/src/dart_notification.rs b/frontend/rust-lib/flowy-grid/src/dart_notification.rs index 7489682619..6e47dfceb9 100644 --- a/frontend/rust-lib/flowy-grid/src/dart_notification.rs +++ b/frontend/rust-lib/flowy-grid/src/dart_notification.rs @@ -33,4 +33,3 @@ impl std::convert::From for i32 { pub fn send_dart_notification(id: &str, ty: GridNotification) -> DartNotifyBuilder { DartNotifyBuilder::new(id, ty, OBSERVABLE_CATEGORY) } - diff --git a/frontend/rust-lib/flowy-grid/src/manager.rs b/frontend/rust-lib/flowy-grid/src/manager.rs index 39f8641c7e..3e7b97f50b 100644 --- a/frontend/rust-lib/flowy-grid/src/manager.rs +++ b/frontend/rust-lib/flowy-grid/src/manager.rs @@ -183,7 +183,14 @@ pub async fn make_grid_view_data( grid_manager: Arc, build_context: BuildGridContext, ) -> FlowyResult { - for block_meta_data in &build_context.blocks { + let BuildGridContext { + field_revs, + block_metas, + blocks, + grid_view_revision_data, + } = build_context; + + for block_meta_data in &blocks { let block_id = &block_meta_data.block_id; // Indexing the block's rows block_meta_data.rows.iter().for_each(|row| { @@ -200,7 +207,7 @@ pub async fn make_grid_view_data( // 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); + let grid_rev = GridRevision::from_build_context(&grid_id, field_revs, block_metas); // Create grid let grid_rev_delta = make_grid_delta(&grid_rev); @@ -210,7 +217,11 @@ 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(grid_id, view_id.to_owned(), layout.into()); + let grid_view = if grid_view_revision_data.is_empty() { + GridViewRevision::new(grid_id, view_id.to_owned(), layout.into()) + } else { + GridViewRevision::from_json(grid_view_revision_data)? + }; 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 aa913c2c4e..b2fe28665e 100644 --- a/frontend/rust-lib/flowy-grid/src/services/grid_editor.rs +++ b/frontend/rust-lib/flowy-grid/src/services/grid_editor.rs @@ -673,6 +673,7 @@ impl GridRevisionEditor { pub async fn duplicate_grid(&self) -> FlowyResult { let grid_pad = self.grid_pad.read().await; + let grid_view_revision_data = self.view_manager.duplicate_grid_view().await?; let original_blocks = grid_pad.get_block_meta_revs(); let (duplicated_fields, duplicated_blocks) = grid_pad.duplicate_grid_block_meta().await; @@ -698,6 +699,7 @@ impl GridRevisionEditor { field_revs: duplicated_fields.into_iter().map(Arc::new).collect(), block_metas: duplicated_blocks, blocks: blocks_meta_data, + grid_view_revision_data, }) } 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 c926743f9f..0889aec43e 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 @@ -74,6 +74,11 @@ impl GridViewRevisionEditor { }) } + pub(crate) async fn duplicate_view_data(&self) -> FlowyResult { + let json_str = self.pad.read().await.json_str()?; + Ok(json_str) + } + pub(crate) async fn will_create_row(&self, row_rev: &mut RowRevision, params: &CreateRowParams) { if params.group_id.is_none() { return; 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 b1041a818c..db7aa53ee2 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 @@ -56,6 +56,12 @@ impl GridViewManager { }) } + pub(crate) async fn duplicate_grid_view(&self) -> FlowyResult { + let editor = self.get_default_view_editor().await?; + let view_data = editor.duplicate_view_data().await?; + Ok(view_data) + } + /// When the row was created, we may need to modify the [RowRevision] according to the [CreateRowParams]. pub(crate) async fn will_create_row(&self, row_rev: &mut RowRevision, params: &CreateRowParams) { for view_editor in self.view_editors.iter() { diff --git a/frontend/rust-lib/flowy-grid/src/services/group/controller.rs b/frontend/rust-lib/flowy-grid/src/services/group/controller.rs index 7a1d7dbe62..f65b87e829 100644 --- a/frontend/rust-lib/flowy-grid/src/services/group/controller.rs +++ b/frontend/rust-lib/flowy-grid/src/services/group/controller.rs @@ -1,10 +1,12 @@ -use crate::entities::{ GroupChangesetPB, GroupViewChangesetPB, InsertedRowPB, RowPB}; -use crate::services::cell::{decode_any_cell_data, CellBytesParser,}; +use crate::entities::{GroupChangesetPB, GroupViewChangesetPB, InsertedRowPB, RowPB}; +use crate::services::cell::{decode_any_cell_data, CellBytesParser}; use crate::services::group::action::GroupAction; use crate::services::group::configuration::GroupContext; use crate::services::group::entities::Group; use flowy_error::FlowyResult; -use flowy_grid_data_model::revision::{ FieldRevision, GroupConfigurationContentSerde, GroupRevision, RowChangeset, RowRevision, TypeOptionDataDeserializer}; +use flowy_grid_data_model::revision::{ + FieldRevision, GroupConfigurationContentSerde, GroupRevision, RowChangeset, RowRevision, TypeOptionDataDeserializer, +}; use std::marker::PhantomData; use std::sync::Arc; diff --git a/frontend/rust-lib/flowy-grid/src/services/group/controller_impls/checkbox_controller.rs b/frontend/rust-lib/flowy-grid/src/services/group/controller_impls/checkbox_controller.rs index aa0a0bec96..a15b0cc62b 100644 --- a/frontend/rust-lib/flowy-grid/src/services/group/controller_impls/checkbox_controller.rs +++ b/frontend/rust-lib/flowy-grid/src/services/group/controller_impls/checkbox_controller.rs @@ -80,7 +80,7 @@ impl GroupAction for CheckboxGroupController { fn move_row(&mut self, _cell_data: &Self::CellDataType, mut context: MoveGroupRowContext) -> Vec { let mut group_changeset = vec![]; self.group_ctx.iter_mut_all_groups(|group| { - if let Some(changeset) = move_group_row(group, &mut context) { + if let Some(changeset) = move_group_row(group, &mut context) { group_changeset.push(changeset); } }); diff --git a/frontend/rust-lib/flowy-grid/src/services/group/controller_impls/select_option_controller/single_select_controller.rs b/frontend/rust-lib/flowy-grid/src/services/group/controller_impls/select_option_controller/single_select_controller.rs index 8ba632cc81..0c86166520 100644 --- a/frontend/rust-lib/flowy-grid/src/services/group/controller_impls/select_option_controller/single_select_controller.rs +++ b/frontend/rust-lib/flowy-grid/src/services/group/controller_impls/select_option_controller/single_select_controller.rs @@ -49,7 +49,7 @@ impl GroupAction for SingleSelectGroupController { fn move_row(&mut self, _cell_data: &Self::CellDataType, mut context: MoveGroupRowContext) -> Vec { let mut group_changeset = vec![]; self.group_ctx.iter_mut_all_groups(|group| { - if let Some(changeset) = move_group_row(group, &mut context) { + if let Some(changeset) = move_group_row(group, &mut context) { group_changeset.push(changeset); } }); 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 91cbfc2355..f9eac21ba0 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 @@ -170,7 +170,7 @@ impl ViewDataProcessor for TextBlockViewDataProcessor { }) } - fn get_delta_data(&self, view_id: &str) -> FutureResult { + fn get_view_data(&self, view_id: &str) -> FutureResult { let view_id = view_id.to_string(); let manager = self.0.clone(); FutureResult::new(async move { @@ -247,7 +247,7 @@ impl ViewDataProcessor for GridViewDataProcessor { }) } - fn get_delta_data(&self, view_id: &str) -> FutureResult { + fn get_view_data(&self, view_id: &str) -> FutureResult { let view_id = view_id.to_string(); let grid_manager = self.0.clone(); FutureResult::new(async move { 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 90187d9b7b..7f1b41782b 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 @@ -34,11 +34,15 @@ impl GridRevision { } } - pub fn from_build_context(grid_id: &str, context: BuildGridContext) -> Self { + pub fn from_build_context( + grid_id: &str, + field_revs: Vec>, + block_metas: Vec, + ) -> Self { Self { grid_id: grid_id.to_owned(), - fields: context.field_revs, - blocks: context.block_metas.into_iter().map(Arc::new).collect(), + fields: field_revs, + blocks: block_metas.into_iter().map(Arc::new).collect(), } } } @@ -188,6 +192,9 @@ pub struct BuildGridContext { pub field_revs: Vec>, pub block_metas: Vec, pub blocks: Vec, + + // String in JSON format. It can be deserialized into [GridViewRevision] + pub grid_view_revision_data: String, } impl BuildGridContext { 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 24b991be6a..06178d23c1 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 @@ -58,6 +58,10 @@ impl GridViewRevision { // row_orders: vec![], } } + + pub fn from_json(json: String) -> Result { + serde_json::from_str(&json) + } } #[derive(Debug, Clone, Default, Serialize, Deserialize)] From dfb7ed10536a403027b05f40ba856c1a659d0115 Mon Sep 17 00:00:00 2001 From: nathan Date: Sat, 24 Sep 2022 21:48:48 +0800 Subject: [PATCH 21/25] fix: switch field type in board --- .../rust-lib/flowy-grid/src/event_handler.rs | 2 +- frontend/rust-lib/flowy-grid/src/macros.rs | 7 ++++++- .../src/services/cell/cell_operation.rs | 10 ++++------ .../flowy-grid/src/services/grid_editor.rs | 17 ++++++++++------- .../src/services/grid_view_manager.rs | 10 ++++++++-- .../flowy-grid/tests/grid/group_test/test.rs | 7 ++----- 6 files changed, 31 insertions(+), 22 deletions(-) diff --git a/frontend/rust-lib/flowy-grid/src/event_handler.rs b/frontend/rust-lib/flowy-grid/src/event_handler.rs index c9b6cf4662..1de9c2ed30 100644 --- a/frontend/rust-lib/flowy-grid/src/event_handler.rs +++ b/frontend/rust-lib/flowy-grid/src/event_handler.rs @@ -44,7 +44,7 @@ pub(crate) async fn update_grid_setting_handler( let editor = manager.get_grid_editor(¶ms.grid_id)?; if let Some(insert_params) = params.insert_group { - let _ = editor.create_group(insert_params).await?; + let _ = editor.insert_group(insert_params).await?; } if let Some(delete_params) = params.delete_group { diff --git a/frontend/rust-lib/flowy-grid/src/macros.rs b/frontend/rust-lib/flowy-grid/src/macros.rs index bee2e95e21..6ca3de52e5 100644 --- a/frontend/rust-lib/flowy-grid/src/macros.rs +++ b/frontend/rust-lib/flowy-grid/src/macros.rs @@ -73,7 +73,12 @@ macro_rules! impl_type_option { match serde_json::from_str(s) { Ok(obj) => obj, Err(err) => { - tracing::error!("{} convert from any data failed, {:?}", stringify!($target), err); + tracing::error!( + "{} type option deserialize from {} failed, {:?}", + stringify!($target), + s, + err + ); $target::default() } } 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 569a5f2f7d..3d6f951dfd 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 @@ -90,12 +90,10 @@ pub fn decode_any_cell_data + Debug> } } } - Err(err) => { - tracing::error!( - "Decode type option data to type: {} failed: {:?}", - std::any::type_name::(), - err, - ); + Err(_err) => { + // It's okay to ignore this error, because it's okay that the current cell can't + // display the existing cell data. For example, the UI of the text cell will be blank if + // the type of the data of cell is Number. CellBytes::default() } } 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 b2fe28665e..ccad53bf5c 100644 --- a/frontend/rust-lib/flowy-grid/src/services/grid_editor.rs +++ b/frontend/rust-lib/flowy-grid/src/services/grid_editor.rs @@ -179,23 +179,20 @@ impl GridRevisionEditor { None => Err(ErrorCode::FieldDoesNotExist.into()), Some(field_type) => { let _ = self.update_field_rev(params, field_type).await?; - match self.view_manager.did_update_field(&field_id).await { - Ok(_) => {} - Err(e) => tracing::error!("View manager update field failed: {:?}", e), - } let _ = self.notify_did_update_grid_field(&field_id).await?; Ok(()) } } } + // Replaces the field revision with new field revision. pub async fn replace_field(&self, field_rev: Arc) -> FlowyResult<()> { let field_id = field_rev.id.clone(); let _ = self .modify(|grid_pad| Ok(grid_pad.replace_field_rev(field_rev.clone())?)) .await?; - match self.view_manager.did_update_field(&field_rev.id).await { + match self.view_manager.did_update_field(&field_rev.id, false).await { Ok(_) => {} Err(e) => tracing::error!("View manager update field failed: {:?}", e), } @@ -279,6 +276,7 @@ impl GridRevisionEditor { } async fn update_field_rev(&self, params: FieldChangesetParams, field_type: FieldType) -> FlowyResult<()> { + let mut is_type_option_changed = false; let _ = self .modify(|grid| { let deserializer = TypeOptionJsonDeserializer(field_type); @@ -319,6 +317,7 @@ impl GridRevisionEditor { Ok(json_str) => { let field_type = field.ty; field.insert_type_option_str(&field_type, json_str); + is_type_option_changed = true; is_changed = Some(()) } Err(err) => { @@ -333,7 +332,11 @@ impl GridRevisionEditor { }) .await?; - match self.view_manager.did_update_field(¶ms.field_id).await { + match self + .view_manager + .did_update_field(¶ms.field_id, is_type_option_changed) + .await + { Ok(_) => {} Err(e) => tracing::error!("View manager update field failed: {:?}", e), } @@ -537,7 +540,7 @@ impl GridRevisionEditor { self.view_manager.get_filters().await } - pub async fn create_group(&self, params: InsertGroupParams) -> FlowyResult<()> { + pub async fn insert_group(&self, params: InsertGroupParams) -> FlowyResult<()> { self.view_manager.insert_or_update_group(params).await } 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 db7aa53ee2..d548cbc9cb 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 @@ -175,9 +175,15 @@ impl GridViewManager { Ok(()) } - pub(crate) async fn did_update_field(&self, field_id: &str) -> FlowyResult<()> { + #[tracing::instrument(level = "trace", skip(self), err)] + pub(crate) async fn did_update_field(&self, field_id: &str, is_type_option_changed: bool) -> FlowyResult<()> { let view_editor = self.get_default_view_editor().await?; - let _ = view_editor.did_update_field(field_id).await?; + if is_type_option_changed { + let _ = view_editor.group_by_field(field_id).await?; + } else { + let _ = view_editor.did_update_field(field_id).await?; + } + Ok(()) } diff --git a/frontend/rust-lib/flowy-grid/tests/grid/group_test/test.rs b/frontend/rust-lib/flowy-grid/tests/grid/group_test/test.rs index 5926f8c022..a205420498 100644 --- a/frontend/rust-lib/flowy-grid/tests/grid/group_test/test.rs +++ b/frontend/rust-lib/flowy-grid/tests/grid/group_test/test.rs @@ -382,11 +382,8 @@ async fn group_insert_single_select_option_test() { AssertGroupCount(5), ]; test.run_scripts(scripts).await; - - // the group at index 4 is the default_group, so the new insert group will be the - // index 3. - let group_3 = test.group_at_index(3).await; - assert_eq!(group_3.desc, new_option_name); + let new_group = test.group_at_index(0).await; + assert_eq!(new_group.desc, new_option_name); } #[tokio::test] From d6d8b4da5a9ddd608b4dee78d87bc1ce93f36ecf Mon Sep 17 00:00:00 2001 From: nathan Date: Sun, 25 Sep 2022 10:57:40 +0800 Subject: [PATCH 22/25] fix: duplicate time format str --- frontend/app_flowy/assets/translations/en.json | 2 +- frontend/app_flowy/assets/translations/es-VE.json | 2 +- frontend/app_flowy/assets/translations/fr-FR.json | 2 +- frontend/app_flowy/assets/translations/id-ID.json | 2 +- frontend/app_flowy/assets/translations/ja-JP.json | 2 +- frontend/app_flowy/assets/translations/pt-BR.json | 14 +++++++------- frontend/app_flowy/assets/translations/ru-RU.json | 8 ++++---- frontend/app_flowy/assets/translations/zh-CN.json | 2 +- frontend/app_flowy/assets/translations/zh-TW.json | 2 +- .../type_options/date_type_option/date_tests.rs | 2 +- .../date_type_option/date_type_option_entities.rs | 2 +- 11 files changed, 20 insertions(+), 20 deletions(-) diff --git a/frontend/app_flowy/assets/translations/en.json b/frontend/app_flowy/assets/translations/en.json index b607180f4f..cd7932945b 100644 --- a/frontend/app_flowy/assets/translations/en.json +++ b/frontend/app_flowy/assets/translations/en.json @@ -181,7 +181,7 @@ "includeTime": " Include time", "dateFormatFriendly": "Month Day,Year", "dateFormatISO": "Year-Month-Day", - "dateFormatLocal": "Year/Month/Day", + "dateFormatLocal": "Month/Day/Year", "dateFormatUS": "Year/Month/Day", "timeFormat": " Time format", "invalidTimeFormat": "Invalid format", diff --git a/frontend/app_flowy/assets/translations/es-VE.json b/frontend/app_flowy/assets/translations/es-VE.json index b07b0a6ef5..869a5e75a3 100644 --- a/frontend/app_flowy/assets/translations/es-VE.json +++ b/frontend/app_flowy/assets/translations/es-VE.json @@ -172,7 +172,7 @@ "includeTime": " Incluir tiempo", "dateFormatFriendly": "Mes Día, Año", "dateFormatISO": "Año-Mes-Día", - "dateFormatLocal": "Año/Mes/Día", + "dateFormatLocal": "Mes/Día/Año", "dateFormatUS": "Año/Mes/Día", "timeFormat": " Time format", "invalidTimeFormat": "Formato de tiempo", diff --git a/frontend/app_flowy/assets/translations/fr-FR.json b/frontend/app_flowy/assets/translations/fr-FR.json index 1408db0343..2a3ad60b7b 100644 --- a/frontend/app_flowy/assets/translations/fr-FR.json +++ b/frontend/app_flowy/assets/translations/fr-FR.json @@ -170,7 +170,7 @@ "includeTime": " Inclure l'heure", "dateFormatFriendly": "Mois Jour, Année", "dateFormatISO": "Année-Mois-Jour", - "dateFormatLocal": "Année/Mois/Jour", + "dateFormatLocal": "Mois/Jour/Année", "dateFormatUS": "Année/Mois/Jour", "timeFormat": " Format du temps", "invalidTimeFormat": "Format invalide", diff --git a/frontend/app_flowy/assets/translations/id-ID.json b/frontend/app_flowy/assets/translations/id-ID.json index 765865b012..1239194266 100644 --- a/frontend/app_flowy/assets/translations/id-ID.json +++ b/frontend/app_flowy/assets/translations/id-ID.json @@ -173,7 +173,7 @@ "includeTime": " Sertakan waktu", "dateFormatFriendly": "Bulan Hari,Tahun", "dateFormatISO": "Tahun-Bulan-Hari", - "dateFormatLocal": "Tahun/Bulan/Hari", + "dateFormatLocal": "Bulan/Hari/Tahun", "dateFormatUS": "Tahun/Bulan/Hari", "timeFormat": " Format waktu", "invalidTimeFormat": "Format yang tidak valid", diff --git a/frontend/app_flowy/assets/translations/ja-JP.json b/frontend/app_flowy/assets/translations/ja-JP.json index f29051263f..793604212c 100644 --- a/frontend/app_flowy/assets/translations/ja-JP.json +++ b/frontend/app_flowy/assets/translations/ja-JP.json @@ -165,7 +165,7 @@ "includeTime": " 時刻を含める", "dateFormatFriendly": "月 日,年", "dateFormatISO": "年-月-日", - "dateFormatLocal": "年/月/日", + "dateFormatLocal": "月/日/年", "dateFormatUS": "年/月/日", "timeFormat": " 時刻書式", "timeFormatTwelveHour": "12 時間表記", diff --git a/frontend/app_flowy/assets/translations/pt-BR.json b/frontend/app_flowy/assets/translations/pt-BR.json index a5ccbeaf2a..4bea9c6a3d 100644 --- a/frontend/app_flowy/assets/translations/pt-BR.json +++ b/frontend/app_flowy/assets/translations/pt-BR.json @@ -63,13 +63,13 @@ "deletePermanent": "Apagar permanentemente" }, "dialogCreatePageNameHint": "Nome da página", - "questionBubble": { + "questionBubble": { "whatsNew": "O que há de novo?", "help": "Ajuda e Suporte", "debug": { - "name": "Informação de depuração", - "success": "Informação de depuração copiada para a área de transferência!", - "fail": "Falha ao copiar a informação de depuração para a área de transferência" + "name": "Informação de depuração", + "success": "Informação de depuração copiada para a área de transferência!", + "fail": "Falha ao copiar a informação de depuração para a área de transferência" } }, "menuAppHeader": { @@ -148,7 +148,7 @@ "menu": { "appearance": "Aparência", "language": "Idioma", - "user":"Usuário", + "user": "Usuário", "open": "Abrir as Configurações" }, "appearance": { @@ -181,7 +181,7 @@ "includeTime": "Incluir horário", "dateFormatFriendly": "Mês/Dia/Ano", "dateFormatISO": "Ano/Mês/Dia", - "dateFormatLocal": "Ano/Mês/Dia", + "dateFormatLocal": "Mês/Dia/Ano", "dateFormatUS": "Ano/Mês/Dia", "timeFormat": "Formato de hora", "invalidTimeFormat": "Formato Inválido", @@ -231,4 +231,4 @@ "create_new_card": "Novo" } } -} +} \ No newline at end of file diff --git a/frontend/app_flowy/assets/translations/ru-RU.json b/frontend/app_flowy/assets/translations/ru-RU.json index 50ffa83a44..6f53886f84 100644 --- a/frontend/app_flowy/assets/translations/ru-RU.json +++ b/frontend/app_flowy/assets/translations/ru-RU.json @@ -94,9 +94,9 @@ }, "tooltip": { "darkMode": "Переключиться в тёмную тему", - "openAsPage": "Открыть как страницу", - "addNewRow": "Добавить новую строку", - "openMenu": "Открыть меню" + "openAsPage": "Открыть как страницу", + "addNewRow": "Добавить новую строку", + "openMenu": "Открыть меню" }, "sideBar": { "closeSidebar": "Закрыть боковое меню", @@ -180,7 +180,7 @@ "includeTime": " Время", "dateFormatFriendly": "День Месяц, Год", "dateFormatISO": "Год-Месяц-День", - "dateFormatLocal": "Год/Месяц/День", + "dateFormatLocal": "Месяц/День/Год", "dateFormatUS": "Год/Месяц/День", "timeFormat": " Форматировать время", "invalidTimeFormat": "Неверный формат", diff --git a/frontend/app_flowy/assets/translations/zh-CN.json b/frontend/app_flowy/assets/translations/zh-CN.json index e5628c8bb8..f5fee6ab23 100644 --- a/frontend/app_flowy/assets/translations/zh-CN.json +++ b/frontend/app_flowy/assets/translations/zh-CN.json @@ -177,7 +177,7 @@ "includeTime": " 包含时间", "dateFormatFriendly": "月 日,年", "dateFormatISO": "年-月-日", - "dateFormatLocal": "年/月/日", + "dateFormatLocal": "月/日/年", "dateFormatUS": "年/月/日", "timeFormat": " 时间格式", "invalidTimeFormat": "时间格式错误", diff --git a/frontend/app_flowy/assets/translations/zh-TW.json b/frontend/app_flowy/assets/translations/zh-TW.json index 6990d1697b..faa2dea03e 100644 --- a/frontend/app_flowy/assets/translations/zh-TW.json +++ b/frontend/app_flowy/assets/translations/zh-TW.json @@ -173,7 +173,7 @@ "includeTime": " 包含時間", "dateFormatFriendly": "月 日,年", "dateFormatISO": "年-月-日", - "dateFormatLocal": "年/月/日", + "dateFormatLocal": "月/日/年", "dateFormatUS": "年/月/日", "timeFormat": " 時間格式", "invalidTimeFormat": "格式無效", 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 d8f034fa3d..2e08b91d1c 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 @@ -24,7 +24,7 @@ mod tests { assert_date(&type_option, 1647251762, None, "2022-03-14", &field_rev); } DateFormat::Local => { - assert_date(&type_option, 1647251762, None, "2022/03/14", &field_rev); + assert_date(&type_option, 1647251762, None, "03/14/2022", &field_rev); } } } 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 aef76b9c65..ff2dc3fc4f 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 @@ -153,7 +153,7 @@ impl DateFormat { // https://docs.rs/chrono/0.4.19/chrono/format/strftime/index.html pub fn format_str(&self) -> &'static str { match self { - DateFormat::Local => "%Y/%m/%d", + DateFormat::Local => "%m/%d/%Y", DateFormat::US => "%Y/%m/%d", DateFormat::ISO => "%Y-%m-%d", DateFormat::Friendly => "%b %d,%Y", From edd9128046a4ec8f4756cfb05c113cc44045b0b3 Mon Sep 17 00:00:00 2001 From: Richard Shiue <71320345+richardshiue@users.noreply.github.com> Date: Sun, 25 Sep 2022 12:32:40 +0800 Subject: [PATCH 23/25] fix: press enter to create or select option in text field --- .../cell/select_option_editor_bloc.dart | 35 +++++++++++++++++++ .../select_option_editor.dart | 4 +-- .../cell/select_option_cell/text_field.dart | 6 ++-- 3 files changed, 40 insertions(+), 5 deletions(-) diff --git a/frontend/app_flowy/lib/plugins/grid/application/cell/select_option_editor_bloc.dart b/frontend/app_flowy/lib/plugins/grid/application/cell/select_option_editor_bloc.dart index 93343d8c4c..b9285890f1 100644 --- a/frontend/app_flowy/lib/plugins/grid/application/cell/select_option_editor_bloc.dart +++ b/frontend/app_flowy/lib/plugins/grid/application/cell/select_option_editor_bloc.dart @@ -54,6 +54,9 @@ class SelectOptionCellEditorBloc selectOption: (_SelectOption value) { _onSelectOption(value.optionId); }, + trySelectOption: (_TrySelectOption value) { + _trySelectOption(value.optionName, emit); + }, filterOption: (_SelectOptionFilter value) { _filterOption(value.optionName, emit); }, @@ -100,6 +103,36 @@ class SelectOptionCellEditorBloc } } + void _trySelectOption( + String optionName, Emitter emit) async { + SelectOptionPB? matchingOption; + bool optionExistsButSelected = false; + + for (final option in state.options) { + if (option.name.toLowerCase() == optionName.toLowerCase()) { + if (!state.selectedOptions.contains(option)) { + matchingOption = option; + break; + } else { + optionExistsButSelected = true; + } + } + } + + // if there isn't a matching option at all, then create it + if (matchingOption == null && !optionExistsButSelected) { + _createOption(optionName); + } + + // if there is an unselected matching option, select it + if (matchingOption != null) { + _selectOptionService.select(optionId: matchingOption.id); + } + + // clear the filter + emit(state.copyWith(filter: none())); + } + void _filterOption(String optionName, Emitter emit) { final _MakeOptionResult result = _makeOptions(Some(optionName), state.allOptions); @@ -187,6 +220,8 @@ class SelectOptionEditorEvent with _$SelectOptionEditorEvent { _DeleteOption; const factory SelectOptionEditorEvent.filterOption(String optionName) = _SelectOptionFilter; + const factory SelectOptionEditorEvent.trySelectOption(String optionName) = + _TrySelectOption; } @freezed diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/select_option_cell/select_option_editor.dart b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/select_option_cell/select_option_editor.dart index 9a0252e009..bc80798fea 100644 --- a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/select_option_cell/select_option_editor.dart +++ b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/select_option_cell/select_option_editor.dart @@ -154,10 +154,10 @@ class _TextField extends StatelessWidget { .read() .add(SelectOptionEditorEvent.filterOption(text)); }, - onNewTag: (tagName) { + onSubmitted: (tagName) { context .read() - .add(SelectOptionEditorEvent.newOption(tagName)); + .add(SelectOptionEditorEvent.trySelectOption(tagName)); }, ), ); 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 f77d56d635..2bb249dbd5 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 @@ -17,7 +17,7 @@ class SelectOptionTextField extends StatefulWidget { final LinkedHashMap selectedOptionMap; final double distanceToText; - final Function(String) onNewTag; + final Function(String) onSubmitted; final Function(String) newText; final VoidCallback? onClick; @@ -26,7 +26,7 @@ class SelectOptionTextField extends StatefulWidget { required this.selectedOptionMap, required this.distanceToText, required this.tagController, - required this.onNewTag, + required this.onSubmitted, required this.newText, this.onClick, TextEditingController? textController, @@ -88,7 +88,7 @@ class _SelectOptionTextFieldState extends State { } if (text.isNotEmpty) { - widget.onNewTag(text); + widget.onSubmitted(text); focusNode.requestFocus(); } }, From 42995b6bafa14c7a811e4df2ca5e6d29d8db1f9a Mon Sep 17 00:00:00 2001 From: nathan Date: Sun, 25 Sep 2022 14:56:54 +0800 Subject: [PATCH 24/25] fix: auto resize Action list --- .../app_flowy/lib/plugins/doc/document.dart | 3 - .../home/menu/app/header/add_button.dart | 3 +- .../widgets/float_bubble/question_bubble.dart | 12 ++-- .../presentation/widgets/pop_up_action.dart | 41 ++++++------ .../example/lib/overlay/overlay_screen.dart | 4 +- .../lib/src/flowy_overlay/list_overlay.dart | 67 +++++++++++-------- 6 files changed, 70 insertions(+), 60 deletions(-) diff --git a/frontend/app_flowy/lib/plugins/doc/document.dart b/frontend/app_flowy/lib/plugins/doc/document.dart index 9ef30f037e..b9cd952bd8 100644 --- a/frontend/app_flowy/lib/plugins/doc/document.dart +++ b/frontend/app_flowy/lib/plugins/doc/document.dart @@ -195,9 +195,6 @@ class ShareActions with ActionList, FlowyOverlayDelegate { ShareActions({required this.onSelected}); - @override - double get maxWidth => 130; - @override double get itemHeight => 22; diff --git a/frontend/app_flowy/lib/workspace/presentation/home/menu/app/header/add_button.dart b/frontend/app_flowy/lib/workspace/presentation/home/menu/app/header/add_button.dart index 5b06d2c3a2..57e0395d8a 100644 --- a/frontend/app_flowy/lib/workspace/presentation/home/menu/app/header/add_button.dart +++ b/frontend/app_flowy/lib/workspace/presentation/home/menu/app/header/add_button.dart @@ -61,8 +61,7 @@ class ActionList { itemBuilder: (context, index) => items[index], anchorContext: anchorContext, anchorDirection: AnchorDirection.bottomRight, - width: 120, - height: 80, + constraints: BoxConstraints.tight(const Size(120, 80)), ); } } diff --git a/frontend/app_flowy/lib/workspace/presentation/widgets/float_bubble/question_bubble.dart b/frontend/app_flowy/lib/workspace/presentation/widgets/float_bubble/question_bubble.dart index 67e5f4ba25..732c05325f 100644 --- a/frontend/app_flowy/lib/workspace/presentation/widgets/float_bubble/question_bubble.dart +++ b/frontend/app_flowy/lib/workspace/presentation/widgets/float_bubble/question_bubble.dart @@ -111,9 +111,6 @@ class QuestionBubbleActionSheet required this.onSelected, }); - @override - double get maxWidth => 170; - @override double get itemHeight => 22; @@ -142,7 +139,7 @@ class QuestionBubbleActionSheet @override ListOverlayFooter? get footer => ListOverlayFooter( widget: const FlowyVersionDescription(), - height: 30, + height: 40, padding: const EdgeInsets.only(top: 6), ); } @@ -174,8 +171,11 @@ class FlowyVersionDescription extends StatelessWidget { children: [ Divider(height: 1, color: theme.shader6, thickness: 1.0), const VSpace(6), - FlowyText("$appName $version.$buildNumber", - fontSize: 12, color: theme.shader4), + FlowyText( + "$appName $version.$buildNumber", + fontSize: 12, + color: theme.shader4, + ), ], ).padding( horizontal: ActionListSizes.itemHPadding + ActionListSizes.padding, diff --git a/frontend/app_flowy/lib/workspace/presentation/widgets/pop_up_action.dart b/frontend/app_flowy/lib/workspace/presentation/widgets/pop_up_action.dart index 64f29036e0..2e43bd4b9d 100644 --- a/frontend/app_flowy/lib/workspace/presentation/widgets/pop_up_action.dart +++ b/frontend/app_flowy/lib/workspace/presentation/widgets/pop_up_action.dart @@ -13,7 +13,9 @@ abstract class ActionList { String get identifier => toString(); - double get maxWidth => 162; + double get maxWidth => 300; + + double get minWidth => 120; double get itemHeight => ActionListSizes.itemHeight; @@ -29,28 +31,29 @@ abstract class ActionList { AnchorDirection anchorDirection = AnchorDirection.bottomRight, Offset? anchorOffset, }) { - final widgets = items - .map( - (action) => ActionCell( - action: action, - itemHeight: itemHeight, - onSelected: (action) { - FlowyOverlay.of(buildContext).remove(identifier); - selectCallback(dartz.some(action)); - }, - ), - ) - .toList(); - ListOverlay.showWithAnchor( buildContext, identifier: identifier, - itemCount: widgets.length, - itemBuilder: (context, index) => widgets[index], + itemCount: items.length, + itemBuilder: (context, index) { + final action = items[index]; + return ActionCell( + action: action, + itemHeight: itemHeight, + onSelected: (action) { + FlowyOverlay.of(buildContext).remove(identifier); + selectCallback(dartz.some(action)); + }, + ); + }, anchorContext: anchorContext ?? buildContext, anchorDirection: anchorDirection, - width: maxWidth, - height: widgets.length * (itemHeight + ActionListSizes.padding * 2), + constraints: BoxConstraints( + minHeight: items.length * (itemHeight + ActionListSizes.padding * 2), + maxHeight: items.length * (itemHeight + ActionListSizes.padding * 2), + maxWidth: maxWidth, + minWidth: minWidth, + ), delegate: delegate, anchorOffset: anchorOffset, footer: footer, @@ -93,7 +96,7 @@ class ActionCell extends StatelessWidget { child: SizedBox( height: itemHeight, child: Row( - crossAxisAlignment: CrossAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, children: [ if (icon != null) icon, HSpace(ActionListSizes.itemHPadding), diff --git a/frontend/app_flowy/packages/flowy_infra_ui/example/lib/overlay/overlay_screen.dart b/frontend/app_flowy/packages/flowy_infra_ui/example/lib/overlay/overlay_screen.dart index cea870a017..ae5cf7ef67 100644 --- a/frontend/app_flowy/packages/flowy_infra_ui/example/lib/overlay/overlay_screen.dart +++ b/frontend/app_flowy/packages/flowy_infra_ui/example/lib/overlay/overlay_screen.dart @@ -218,8 +218,8 @@ class OverlayScreen extends StatelessWidget { overlapBehaviour: providerContext .read() .overlapBehaviour, - width: 200.0, - height: 200.0, + constraints: + BoxConstraints.tight(const Size(200, 200)), ); }, child: const Text('Show List Overlay'), diff --git a/frontend/app_flowy/packages/flowy_infra_ui/lib/src/flowy_overlay/list_overlay.dart b/frontend/app_flowy/packages/flowy_infra_ui/lib/src/flowy_overlay/list_overlay.dart index 24483a149f..85e60926c7 100644 --- a/frontend/app_flowy/packages/flowy_infra_ui/lib/src/flowy_overlay/list_overlay.dart +++ b/frontend/app_flowy/packages/flowy_infra_ui/lib/src/flowy_overlay/list_overlay.dart @@ -1,3 +1,5 @@ +import 'dart:math'; + import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; import 'package:flowy_infra/theme.dart'; @@ -19,46 +21,55 @@ class ListOverlay extends StatelessWidget { const ListOverlay({ Key? key, required this.itemBuilder, - this.itemCount, + this.itemCount = 0, this.controller, - this.width = double.infinity, - this.height = double.infinity, + this.constraints = const BoxConstraints(), this.footer, }) : super(key: key); final IndexedWidgetBuilder itemBuilder; - final int? itemCount; + final int itemCount; final ScrollController? controller; - final double width; - final double height; + final BoxConstraints constraints; final ListOverlayFooter? footer; @override Widget build(BuildContext context) { const padding = EdgeInsets.symmetric(horizontal: 6, vertical: 6); - double totalHeight = height + padding.vertical; + double totalHeight = constraints.minHeight + padding.vertical; if (footer != null) { totalHeight = totalHeight + footer!.height + footer!.padding.vertical; } + final innerConstraints = BoxConstraints( + minHeight: totalHeight, + maxHeight: max(constraints.maxHeight, totalHeight), + minWidth: constraints.minWidth, + maxWidth: constraints.maxWidth, + ); + + List children = []; + for (var i = 0; i < itemCount; i++) { + children.add(itemBuilder(context, i)); + } + return OverlayContainer( - constraints: BoxConstraints.tight(Size(width, totalHeight)), + constraints: innerConstraints, padding: padding, child: SingleChildScrollView( - child: Column( - children: [ - ListView.builder( - shrinkWrap: true, - itemBuilder: itemBuilder, - itemCount: itemCount, - controller: controller, - ), - if (footer != null) - Padding( - padding: footer!.padding, - child: footer!.widget, - ), - ], + scrollDirection: Axis.horizontal, + child: IntrinsicWidth( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + ...children, + if (footer != null) + Padding( + padding: footer!.padding, + child: footer!.widget, + ), + ], + ), ), ), ); @@ -68,10 +79,9 @@ class ListOverlay extends StatelessWidget { BuildContext context, { required String identifier, required IndexedWidgetBuilder itemBuilder, - int? itemCount, + int itemCount = 0, ScrollController? controller, - double width = double.infinity, - double height = double.infinity, + BoxConstraints constraints = const BoxConstraints(), required BuildContext anchorContext, AnchorDirection? anchorDirection, FlowyOverlayDelegate? delegate, @@ -85,8 +95,7 @@ class ListOverlay extends StatelessWidget { itemBuilder: itemBuilder, itemCount: itemCount, controller: controller, - width: width, - height: height, + constraints: constraints, footer: footer, ), identifier: identifier, @@ -122,7 +131,9 @@ class OverlayContainer extends StatelessWidget { child: Container( padding: padding, decoration: FlowyDecoration.decoration( - theme.surface, theme.shadowColor.withOpacity(0.15)), + theme.surface, + theme.shadowColor.withOpacity(0.15), + ), constraints: constraints, child: child, ), From 62c2da06d4783e4f67442b982d5dc9b48470754a Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Sun, 25 Sep 2022 15:55:28 +0800 Subject: [PATCH 25/25] chore: remove the selection_menu_widget_test.dart --- .../selection_menu_item_widget_test.dart | 49 ------------------- .../selection_menu_widget_test.dart | 1 - 2 files changed, 50 deletions(-) delete mode 100644 frontend/app_flowy/packages/appflowy_editor/test/render/selection_menu/selection_menu_item_widget_test.dart diff --git a/frontend/app_flowy/packages/appflowy_editor/test/render/selection_menu/selection_menu_item_widget_test.dart b/frontend/app_flowy/packages/appflowy_editor/test/render/selection_menu/selection_menu_item_widget_test.dart deleted file mode 100644 index 01c1403738..0000000000 --- a/frontend/app_flowy/packages/appflowy_editor/test/render/selection_menu/selection_menu_item_widget_test.dart +++ /dev/null @@ -1,49 +0,0 @@ -import 'package:appflowy_editor/src/render/selection_menu/selection_menu_item_widget.dart'; -import 'package:appflowy_editor/src/render/selection_menu/selection_menu_service.dart'; -import 'package:appflowy_editor/src/render/selection_menu/selection_menu_widget.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; -import '../../infra/test_editor.dart'; - -void main() async { - setUpAll(() { - TestWidgetsFlutterBinding.ensureInitialized(); - }); - - group('selection_menu_item_widget.dart', () { - testWidgets('test selection menu item widget', (tester) async { - bool flag = false; - final editorState = tester.editor.editorState; - final menuService = _TestSelectionMenuService(); - const icon = Icon(Icons.abc); - final item = SelectionMenuItem( - name: 'example', - icon: icon, - keywords: ['example A', 'example B'], - handler: (editorState, menuService, context) { - flag = true; - }, - ); - final widget = SelectionMenuItemWidget( - editorState: editorState, - menuService: menuService, - item: item, - isSelected: true, - ); - await tester.pumpWidget(MaterialApp(home: widget)); - await tester.tap(find.byType(SelectionMenuItemWidget)); - expect(flag, true); - }); - }); -} - -class _TestSelectionMenuService implements SelectionMenuService { - @override - void dismiss() {} - - @override - void show() {} - - @override - Offset get topLeft => throw UnimplementedError(); -} diff --git a/frontend/app_flowy/packages/appflowy_editor/test/render/selection_menu/selection_menu_widget_test.dart b/frontend/app_flowy/packages/appflowy_editor/test/render/selection_menu/selection_menu_widget_test.dart index cc9a2fbd0b..6ac10d593a 100644 --- a/frontend/app_flowy/packages/appflowy_editor/test/render/selection_menu/selection_menu_widget_test.dart +++ b/frontend/app_flowy/packages/appflowy_editor/test/render/selection_menu/selection_menu_widget_test.dart @@ -1,5 +1,4 @@ import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:appflowy_editor/src/document/built_in_attribute_keys.dart'; import 'package:appflowy_editor/src/render/selection_menu/selection_menu_item_widget.dart'; import 'package:appflowy_editor/src/render/selection_menu/selection_menu_service.dart'; import 'package:appflowy_editor/src/render/selection_menu/selection_menu_widget.dart';