fix: turn into issues (#6576)

* fix: cover title issues

* fix: the selection should be cleared if selecting child node

* fix: exclude the blocks that are not supported in the 'turn into' types

* fix: add logs

* fix: floating toolbar ai status

* test: selecting the parent should deselect all the child nodes as well

* chore: 'Copy Link' to 'Copy link'

* fix: select all and turn into block doesn't work on Windows

* test: calculate turn into selection test

* fix: option button tests
This commit is contained in:
Lucas 2024-10-18 17:13:38 +08:00 committed by GitHub
parent ec408940e4
commit 7b031b228e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 661 additions and 294 deletions

View File

@ -2,6 +2,7 @@ import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/callout/callout_block_component.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
@ -143,5 +144,37 @@ void main() {
);
}
});
testWidgets(
'selecting the parent should deselect all the child nodes as well',
(tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
const name = 'Test Document';
await tester.createNewPageWithNameUnderParent(name: name);
await tester.openPage(name);
// create a nested list
// Item 1
// Nested Item 1
await tester.editor.tapLineOfEditorAt(0);
await tester.ime.insertText('Item 1');
await tester.ime.insertCharacter('\n');
await tester.simulateKeyEvent(LogicalKeyboardKey.tab);
await tester.ime.insertText('Nested Item 1');
// select the 'Nested Item 1' and then tap the option button of the 'Item 1'
final editorState = tester.editor.getCurrentEditorState();
final selection = Selection.collapsed(
Position(path: [0, 0], offset: 1),
);
editorState.selection = selection;
await tester.pumpAndSettle();
expect(editorState.selection, selection);
await tester.editor.hoverAndClickOptionMenuButton([0]);
expect(editorState.selection, Selection.collapsed(Position(path: [0])));
},
);
});
}

View File

@ -8,7 +8,6 @@ import 'document_create_and_delete_test.dart'
import 'document_inline_page_reference_test.dart'
as document_inline_page_reference_test;
import 'document_more_actions_test.dart' as document_more_actions_test;
import 'document_option_action_test.dart' as document_option_action_test;
import 'document_shortcuts_test.dart' as document_shortcuts_test;
import 'document_text_direction_test.dart' as document_text_direction_test;
import 'document_with_cover_image_test.dart' as document_with_cover_image_test;
@ -40,7 +39,6 @@ void main() {
document_codeblock_paste_test.main();
document_alignment_test.main();
document_text_direction_test.main();
document_option_action_test.main();
document_with_image_block_test.main();
document_with_multi_image_block_test.main();
document_inline_page_reference_test.main();

View File

@ -252,6 +252,10 @@ class BlockActionOptionCubit extends Cubit<BlockActionOptionState> {
);
insertedNode.add(afterNode);
insertedNode.addAll(node.children.map((e) => e.copyWith()));
} else if (!EditorOptionActionType.turnInto.supportTypes
.contains(node.type)) {
afterNode = node.copyWith();
insertedNode.add(afterNode);
} else {
insertedNode.add(afterNode);
}
@ -267,4 +271,35 @@ class BlockActionOptionCubit extends Cubit<BlockActionOptionState> {
return true;
}
Selection? calculateTurnIntoSelection(
Node selectedNode,
Selection? beforeSelection,
) {
final path = selectedNode.path;
final selection = Selection.collapsed(
Position(path: path),
);
// if the previous selection is null or the start path is not in the same level as the current block path,
// then update the selection with the current block path
// for example,'|' means the selection,
// case 1: collapsed selection
// - bulleted item 1
// - bulleted |item 2
// when clicking the bulleted item 1, the bulleted item 1 path should be selected
// case 2: not collapsed selection
// - bulleted item 1
// - bulleted |item 2
// - bulleted |item 3
// when clicking the bulleted item 1, the bulleted item 1 path should be selected
if (beforeSelection == null ||
beforeSelection.start.path.length != path.length ||
!path.inSelection(beforeSelection)) {
return selection;
}
// if the beforeSelection start with the current block,
// then updating the selection with the beforeSelection that may contains multiple blocks
return beforeSelection;
}
}

View File

@ -1,21 +1,12 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/presentation/editor_notification.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/block_action_button.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/drag_to_reorder/util.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/drag_to_reorder/visual_drag_area.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/base/string_extension.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/custom_image_block_component/custom_image_block_component.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/multi_image_block_component/multi_image_block_component.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/user/application/reminder/reminder_bloc.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'draggable_option_button_feedback.dart';
import 'option_button.dart';
// this flag is used to disable the tooltip of the block when it is dragged
@visibleForTesting
@ -66,13 +57,13 @@ class _DraggableOptionButtonState extends State<DraggableOptionButton> {
onDragStarted: _onDragStart,
onDragUpdate: _onDragUpdate,
onDragEnd: _onDragEnd,
feedback: _OptionButtonFeedback(
feedback: DraggleOptionButtonFeedback(
controller: widget.controller,
editorState: widget.editorState,
blockComponentContext: widget.blockComponentContext,
blockComponentBuilder: widget.blockComponentBuilder,
),
child: _OptionButton(
child: OptionButton(
isDragging: isDraggingAppFlowyEditorBlock,
controller: widget.controller,
editorState: widget.editorState,
@ -130,251 +121,3 @@ class _DraggableOptionButtonState extends State<DraggableOptionButton> {
});
}
}
class _OptionButtonFeedback extends StatefulWidget {
const _OptionButtonFeedback({
required this.controller,
required this.editorState,
required this.blockComponentContext,
required this.blockComponentBuilder,
});
final PopoverController controller;
final EditorState editorState;
final BlockComponentContext blockComponentContext;
final Map<String, BlockComponentBuilder> blockComponentBuilder;
@override
State<_OptionButtonFeedback> createState() => _OptionButtonFeedbackState();
}
class _OptionButtonFeedbackState extends State<_OptionButtonFeedback> {
late Node node;
late BlockComponentContext blockComponentContext;
@override
void initState() {
super.initState();
_setupLockComponentContext();
widget.blockComponentContext.node.addListener(_updateBlockComponentContext);
}
@override
void dispose() {
widget.blockComponentContext.node
.removeListener(_updateBlockComponentContext);
super.dispose();
}
@override
Widget build(BuildContext context) {
final maxWidth = (widget.editorState.renderBox?.size.width ??
MediaQuery.of(context).size.width) *
0.8;
return Opacity(
opacity: 0.7,
child: Material(
color: Colors.transparent,
child: Container(
constraints: BoxConstraints(
maxWidth: maxWidth,
),
child: IntrinsicHeight(
child: Provider.value(
value: widget.editorState,
child: _buildBlock(),
),
),
),
),
);
}
Widget _buildBlock() {
final node = widget.blockComponentContext.node;
final builder = widget.blockComponentBuilder[node.type];
if (builder == null) {
return const SizedBox.shrink();
}
const unsupportedRenderBlockTypes = [
TableBlockKeys.type,
CustomImageBlockKeys.type,
MultiImageBlockKeys.type,
FileBlockKeys.type,
DatabaseBlockKeys.boardType,
DatabaseBlockKeys.calendarType,
DatabaseBlockKeys.gridType,
];
if (unsupportedRenderBlockTypes.contains(node.type)) {
// unable to render table block without provider/context
// render a placeholder instead
return Container(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
decoration: BoxDecoration(
color: Theme.of(context).cardColor,
borderRadius: BorderRadius.circular(8),
),
child: FlowyText(node.type.replaceAll('_', ' ').capitalize()),
);
}
return IntrinsicHeight(
child: MultiProvider(
providers: [
Provider.value(value: widget.editorState),
Provider.value(value: getIt<ReminderBloc>()),
],
child: builder.build(blockComponentContext),
),
);
}
void _updateBlockComponentContext() {
setState(() => _setupLockComponentContext());
}
void _setupLockComponentContext() {
node = widget.blockComponentContext.node.copyWith();
blockComponentContext = BlockComponentContext(
widget.blockComponentContext.buildContext,
node,
);
}
}
class _OptionButton extends StatefulWidget {
const _OptionButton({
required this.controller,
required this.editorState,
required this.blockComponentContext,
required this.isDragging,
});
final PopoverController controller;
final EditorState editorState;
final BlockComponentContext blockComponentContext;
final ValueNotifier<bool> isDragging;
@override
State<_OptionButton> createState() => _OptionButtonState();
}
const _interceptorKey = 'document_option_button_interceptor';
class _OptionButtonState extends State<_OptionButton> {
late final gestureInterceptor = SelectionGestureInterceptor(
key: _interceptorKey,
canTap: (details) => !_isTapInBounds(details.globalPosition),
);
// the selection will be cleared when tap the option button
// so we need to restore the selection after tap the option button
Selection? beforeSelection;
RenderBox? get renderBox => context.findRenderObject() as RenderBox?;
@override
void initState() {
super.initState();
widget.editorState.service.selectionService.registerGestureInterceptor(
gestureInterceptor,
);
}
@override
void dispose() {
widget.editorState.service.selectionService.unregisterGestureInterceptor(
_interceptorKey,
);
super.dispose();
}
@override
Widget build(BuildContext context) {
return ValueListenableBuilder(
valueListenable: widget.isDragging,
builder: (context, isDragging, child) {
return BlockActionButton(
svg: FlowySvgs.drag_element_s,
showTooltip: !isDragging,
richMessage: TextSpan(
children: [
TextSpan(
text: LocaleKeys.document_plugins_optionAction_drag.tr(),
style: context.tooltipTextStyle(),
),
TextSpan(
text: LocaleKeys.document_plugins_optionAction_toMove.tr(),
style: context.tooltipTextStyle(),
),
const TextSpan(text: '\n'),
TextSpan(
text: LocaleKeys.document_plugins_optionAction_click.tr(),
style: context.tooltipTextStyle(),
),
TextSpan(
text: LocaleKeys.document_plugins_optionAction_toOpenMenu.tr(),
style: context.tooltipTextStyle(),
),
],
),
onPointerDown: () {
if (widget.editorState.selection != null) {
beforeSelection = widget.editorState.selection;
}
},
onTap: () {
if (widget.editorState.selection != null) {
beforeSelection = widget.editorState.selection;
}
widget.controller.show();
// update selection
_updateBlockSelection();
},
);
},
);
}
void _updateBlockSelection() {
if (beforeSelection == null) {
final path = widget.blockComponentContext.node.path;
final selection = Selection.collapsed(
Position(path: path),
);
widget.editorState.updateSelectionWithReason(
selection,
customSelectionType: SelectionType.block,
);
} else {
widget.editorState.updateSelectionWithReason(
beforeSelection!,
customSelectionType: SelectionType.block,
);
}
}
bool _isTapInBounds(Offset offset) {
if (renderBox == null) {
return false;
}
final localPosition = renderBox!.globalToLocal(offset);
final result = renderBox!.paintBounds.contains(localPosition);
if (result) {
beforeSelection = widget.editorState.selection;
} else {
beforeSelection = null;
}
return result;
}
}

View File

@ -0,0 +1,266 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/block_action_button.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/base/string_extension.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/custom_image_block_component/custom_image_block_component.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/multi_image_block_component/multi_image_block_component.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/user/application/reminder/reminder_bloc.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class DraggleOptionButtonFeedback extends StatefulWidget {
const DraggleOptionButtonFeedback({
super.key,
required this.controller,
required this.editorState,
required this.blockComponentContext,
required this.blockComponentBuilder,
});
final PopoverController controller;
final EditorState editorState;
final BlockComponentContext blockComponentContext;
final Map<String, BlockComponentBuilder> blockComponentBuilder;
@override
State<DraggleOptionButtonFeedback> createState() =>
_DraggleOptionButtonFeedbackState();
}
class _DraggleOptionButtonFeedbackState
extends State<DraggleOptionButtonFeedback> {
late Node node;
late BlockComponentContext blockComponentContext;
@override
void initState() {
super.initState();
_setupLockComponentContext();
widget.blockComponentContext.node.addListener(_updateBlockComponentContext);
}
@override
void dispose() {
widget.blockComponentContext.node
.removeListener(_updateBlockComponentContext);
super.dispose();
}
@override
Widget build(BuildContext context) {
final maxWidth = (widget.editorState.renderBox?.size.width ??
MediaQuery.of(context).size.width) *
0.8;
return Opacity(
opacity: 0.7,
child: Material(
color: Colors.transparent,
child: Container(
constraints: BoxConstraints(
maxWidth: maxWidth,
),
child: IntrinsicHeight(
child: Provider.value(
value: widget.editorState,
child: _buildBlock(),
),
),
),
),
);
}
Widget _buildBlock() {
final node = widget.blockComponentContext.node;
final builder = widget.blockComponentBuilder[node.type];
if (builder == null) {
return const SizedBox.shrink();
}
const unsupportedRenderBlockTypes = [
TableBlockKeys.type,
CustomImageBlockKeys.type,
MultiImageBlockKeys.type,
FileBlockKeys.type,
DatabaseBlockKeys.boardType,
DatabaseBlockKeys.calendarType,
DatabaseBlockKeys.gridType,
];
if (unsupportedRenderBlockTypes.contains(node.type)) {
// unable to render table block without provider/context
// render a placeholder instead
return Container(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
decoration: BoxDecoration(
color: Theme.of(context).cardColor,
borderRadius: BorderRadius.circular(8),
),
child: FlowyText(node.type.replaceAll('_', ' ').capitalize()),
);
}
return IntrinsicHeight(
child: MultiProvider(
providers: [
Provider.value(value: widget.editorState),
Provider.value(value: getIt<ReminderBloc>()),
],
child: builder.build(blockComponentContext),
),
);
}
void _updateBlockComponentContext() {
setState(() => _setupLockComponentContext());
}
void _setupLockComponentContext() {
node = widget.blockComponentContext.node.copyWith();
blockComponentContext = BlockComponentContext(
widget.blockComponentContext.buildContext,
node,
);
}
}
class _OptionButton extends StatefulWidget {
const _OptionButton({
required this.controller,
required this.editorState,
required this.blockComponentContext,
required this.isDragging,
});
final PopoverController controller;
final EditorState editorState;
final BlockComponentContext blockComponentContext;
final ValueNotifier<bool> isDragging;
@override
State<_OptionButton> createState() => _OptionButtonState();
}
const _interceptorKey = 'document_option_button_interceptor';
class _OptionButtonState extends State<_OptionButton> {
late final gestureInterceptor = SelectionGestureInterceptor(
key: _interceptorKey,
canTap: (details) => !_isTapInBounds(details.globalPosition),
);
// the selection will be cleared when tap the option button
// so we need to restore the selection after tap the option button
Selection? beforeSelection;
RenderBox? get renderBox => context.findRenderObject() as RenderBox?;
@override
void initState() {
super.initState();
widget.editorState.service.selectionService.registerGestureInterceptor(
gestureInterceptor,
);
}
@override
void dispose() {
widget.editorState.service.selectionService.unregisterGestureInterceptor(
_interceptorKey,
);
super.dispose();
}
@override
Widget build(BuildContext context) {
return ValueListenableBuilder(
valueListenable: widget.isDragging,
builder: (context, isDragging, child) {
return BlockActionButton(
svg: FlowySvgs.drag_element_s,
showTooltip: !isDragging,
richMessage: TextSpan(
children: [
TextSpan(
text: LocaleKeys.document_plugins_optionAction_drag.tr(),
style: context.tooltipTextStyle(),
),
TextSpan(
text: LocaleKeys.document_plugins_optionAction_toMove.tr(),
style: context.tooltipTextStyle(),
),
const TextSpan(text: '\n'),
TextSpan(
text: LocaleKeys.document_plugins_optionAction_click.tr(),
style: context.tooltipTextStyle(),
),
TextSpan(
text: LocaleKeys.document_plugins_optionAction_toOpenMenu.tr(),
style: context.tooltipTextStyle(),
),
],
),
onPointerDown: () {
if (widget.editorState.selection != null) {
beforeSelection = widget.editorState.selection;
}
},
onTap: () {
if (widget.editorState.selection != null) {
beforeSelection = widget.editorState.selection;
}
widget.controller.show();
// update selection
_updateBlockSelection();
},
);
},
);
}
void _updateBlockSelection() {
if (beforeSelection == null) {
final path = widget.blockComponentContext.node.path;
final selection = Selection.collapsed(
Position(path: path),
);
widget.editorState.updateSelectionWithReason(
selection,
customSelectionType: SelectionType.block,
);
} else {
widget.editorState.updateSelectionWithReason(
beforeSelection!,
customSelectionType: SelectionType.block,
);
}
}
bool _isTapInBounds(Offset offset) {
if (renderBox == null) {
return false;
}
final localPosition = renderBox!.globalToLocal(offset);
final result = renderBox!.paintBounds.contains(localPosition);
if (result) {
beforeSelection = widget.editorState.selection;
} else {
beforeSelection = null;
}
return result;
}
}

View File

@ -0,0 +1,138 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/block_action_button.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/block_action_option_cubit.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
const _interceptorKey = 'document_option_button_interceptor';
class OptionButton extends StatefulWidget {
const OptionButton({
super.key,
required this.controller,
required this.editorState,
required this.blockComponentContext,
required this.isDragging,
});
final PopoverController controller;
final EditorState editorState;
final BlockComponentContext blockComponentContext;
final ValueNotifier<bool> isDragging;
@override
State<OptionButton> createState() => _OptionButtonState();
}
class _OptionButtonState extends State<OptionButton> {
late final gestureInterceptor = SelectionGestureInterceptor(
key: _interceptorKey,
canTap: (details) => !_isTapInBounds(details.globalPosition),
);
// the selection will be cleared when tap the option button
// so we need to restore the selection after tap the option button
Selection? beforeSelection;
RenderBox? get renderBox => context.findRenderObject() as RenderBox?;
@override
void initState() {
super.initState();
widget.editorState.service.selectionService.registerGestureInterceptor(
gestureInterceptor,
);
}
@override
void dispose() {
widget.editorState.service.selectionService.unregisterGestureInterceptor(
_interceptorKey,
);
super.dispose();
}
@override
Widget build(BuildContext context) {
return ValueListenableBuilder(
valueListenable: widget.isDragging,
builder: (context, isDragging, child) {
return BlockActionButton(
svg: FlowySvgs.drag_element_s,
showTooltip: !isDragging,
richMessage: TextSpan(
children: [
TextSpan(
text: LocaleKeys.document_plugins_optionAction_drag.tr(),
style: context.tooltipTextStyle(),
),
TextSpan(
text: LocaleKeys.document_plugins_optionAction_toMove.tr(),
style: context.tooltipTextStyle(),
),
const TextSpan(text: '\n'),
TextSpan(
text: LocaleKeys.document_plugins_optionAction_click.tr(),
style: context.tooltipTextStyle(),
),
TextSpan(
text: LocaleKeys.document_plugins_optionAction_toOpenMenu.tr(),
style: context.tooltipTextStyle(),
),
],
),
onTap: () {
final selection = widget.editorState.selection;
if (selection != null) {
beforeSelection = selection.normalized;
}
widget.controller.show();
// update selection
_updateBlockSelection(context);
},
);
},
);
}
void _updateBlockSelection(BuildContext context) {
final cubit = context.read<BlockActionOptionCubit>();
final selection = cubit.calculateTurnIntoSelection(
widget.blockComponentContext.node,
beforeSelection,
);
Log.info(
'update block selection, beforeSelection: $beforeSelection, afterSelection: $selection',
);
widget.editorState.updateSelectionWithReason(
selection,
customSelectionType: SelectionType.block,
);
}
bool _isTapInBounds(Offset offset) {
final renderBox = this.renderBox;
if (renderBox == null) {
return false;
}
final localPosition = renderBox.globalToLocal(offset);
final result = renderBox.paintBounds.contains(localPosition);
if (result) {
beforeSelection = widget.editorState.selection?.normalized;
} else {
beforeSelection = null;
}
return result;
}
}

View File

@ -22,8 +22,8 @@ KeyEventResult _backspaceToTitle({
required EditorState editorState,
}) {
final coverTitleFocusNode = editorState.document.root.context
?.read<SharedEditorContext>()
.coverTitleFocusNode;
?.read<SharedEditorContext?>()
?.coverTitleFocusNode;
if (coverTitleFocusNode == null) {
return KeyEventResult.ignored;
}
@ -108,8 +108,8 @@ KeyEventResult _arrowKeyToTitle({
required bool Function(Selection selection) checkSelection,
}) {
final coverTitleFocusNode = editorState.document.root.context
?.read<SharedEditorContext>()
.coverTitleFocusNode;
?.read<SharedEditorContext?>()
?.coverTitleFocusNode;
if (coverTitleFocusNode == null) {
return KeyEventResult.ignored;
}

View File

@ -110,6 +110,7 @@ class _MentionPageBlockState extends State<MentionPageBlock> {
if (UniversalPlatform.isMobile) {
return _MobileMentionPageBlock(
view: view,
content: state.blockContent,
textStyle: widget.textStyle,
handleTap: () => handleTap(view),
handleDoubleTap: handleDoubleTap,
@ -367,6 +368,7 @@ class _NoAccessMentionPageBlock extends StatelessWidget {
class _MobileMentionPageBlock extends StatelessWidget {
const _MobileMentionPageBlock({
required this.view,
required this.content,
required this.textStyle,
required this.handleTap,
required this.handleDoubleTap,
@ -374,6 +376,7 @@ class _MobileMentionPageBlock extends StatelessWidget {
final TextStyle? textStyle;
final ViewPB view;
final String content;
final VoidCallback handleTap;
final VoidCallback handleDoubleTap;
@ -385,6 +388,7 @@ class _MobileMentionPageBlock extends StatelessWidget {
behavior: HitTestBehavior.opaque,
child: _MentionPageBlockContent(
view: view,
content: content,
textStyle: textStyle,
),
);

View File

@ -1,16 +1,16 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/application/document_bloc.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/widgets/smart_edit_action.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
import 'package:appflowy/user/application/user_service.dart';
import 'package:appflowy/workspace/presentation/home/toast.dart';
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart';
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
const _kSmartEditToolbarItemId = 'appflowy.editor.smart_edit';
@ -39,21 +39,12 @@ class SmartEditActionList extends StatefulWidget {
}
class _SmartEditActionListState extends State<SmartEditActionList> {
bool isAIEnabled = false;
bool isAIEnabled = true;
@override
void initState() {
super.initState();
UserBackendService.getCurrentUserProfile().then((value) {
setState(() {
isAIEnabled = value.fold(
(userProfile) =>
userProfile.authenticator == AuthenticatorPB.AppFlowyCloud,
(_) => false,
);
});
});
isAIEnabled = _isAIEnabled();
}
@override
@ -83,12 +74,13 @@ class _SmartEditActionListState extends State<SmartEditActionList> {
),
onTap: () {
if (isAIEnabled) {
keepEditorFocusNotifier.increase();
controller.show();
} else {
showSnackBarMessage(
showToastNotification(
context,
LocaleKeys.document_plugins_appflowyAIEditDisabled.tr(),
showCancel: true,
message:
LocaleKeys.document_plugins_appflowyAIEditDisabled.tr(),
);
}
},
@ -161,4 +153,12 @@ class _SmartEditActionListState extends State<SmartEditActionList> {
}
return res;
}
bool _isAIEnabled() {
final documentContext = widget.editorState.document.root.context;
if (documentContext == null) {
return true;
}
return !documentContext.read<DocumentBloc>().isLocalMode;
}
}

View File

@ -1535,10 +1535,10 @@ packages:
dependency: transitive
description:
name: platform
sha256: "12220bb4b65720483f8fa9450b4332347737cf8213dd2840d8b2c823e47243ec"
sha256: "9b71283fc13df574056616011fb138fd3b793ea47cc509c189a6c3fa5f8a1a65"
url: "https://pub.dev"
source: hosted
version: "3.1.4"
version: "3.1.5"
plugin_platform_interface:
dependency: "direct dev"
description:
@ -1933,10 +1933,10 @@ packages:
dependency: transitive
description:
name: string_scanner
sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde"
sha256: "688af5ed3402a4bde5b3a6c15fd768dbf2621a614950b17f04626c431ab3c4c3"
url: "https://pub.dev"
source: hosted
version: "1.2.0"
version: "1.3.0"
string_validator:
dependency: "direct main"
description:
@ -2238,10 +2238,10 @@ packages:
dependency: transitive
description:
name: vm_service
sha256: "3923c89304b715fb1eb6423f017651664a03bf5f4b29983627c4da791f74a4ec"
sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d"
url: "https://pub.dev"
source: hosted
version: "14.2.1"
version: "14.2.5"
watcher:
dependency: transitive
description:

View File

@ -533,5 +533,155 @@ void main() {
},
);
});
test('calculate selection when turn into', () {
// Example:
// - bulleted list item 1
// - bulleted list item 1-1
// - bulleted list item 1-2
// - bulleted list item 2
// - bulleted list item 2-1
// - bulleted list item 2-2
// - bulleted list item 3
// - bulleted list item 3-1
// - bulleted list item 3-2
const text = 'bulleted list';
const nestedText = 'nested bulleted list';
final document = createDocument([
bulletedListNode(
text: '$text 1',
children: [
bulletedListNode(text: '$nestedText 1-1'),
bulletedListNode(text: '$nestedText 1-2'),
],
),
bulletedListNode(
text: '$text 2',
children: [
bulletedListNode(text: '$nestedText 2-1'),
bulletedListNode(text: '$nestedText 2-2'),
],
),
bulletedListNode(
text: '$text 3',
children: [
bulletedListNode(text: '$nestedText 3-1'),
bulletedListNode(text: '$nestedText 3-2'),
],
),
]);
final editorState = EditorState(document: document);
final cubit = BlockActionOptionCubit(
editorState: editorState,
blockComponentBuilder: {},
);
// case 1: collapsed selection and the selection is in the top level
// and tap the turn into button at the [0]
final selection1 = Selection.collapsed(
Position(path: [0], offset: 1),
);
expect(
cubit.calculateTurnIntoSelection(
editorState.getNodeAtPath([0])!,
selection1,
),
selection1,
);
// case 2: collapsed selection and the selection is in the nested level
// and tap the turn into button at the [0]
final selection2 = Selection.collapsed(
Position(path: [0, 0], offset: 1),
);
expect(
cubit.calculateTurnIntoSelection(
editorState.getNodeAtPath([0])!,
selection2,
),
Selection.collapsed(Position(path: [0])),
);
// case 3, collapsed selection and the selection is in the nested level
// and tap the turn into button at the [0, 0]
final selection3 = Selection.collapsed(
Position(path: [0, 0], offset: 1),
);
expect(
cubit.calculateTurnIntoSelection(
editorState.getNodeAtPath([0, 0])!,
selection3,
),
selection3,
);
// case 4, not collapsed selection and the selection is in the top level
// and tap the turn into button at the [0]
final selection4 = Selection(
start: Position(path: [0], offset: 1),
end: Position(path: [1], offset: 1),
);
expect(
cubit.calculateTurnIntoSelection(
editorState.getNodeAtPath([0])!,
selection4,
),
selection4,
);
// case 5, not collapsed selection and the selection is in the nested level
// and tap the turn into button at the [0]
final selection5 = Selection(
start: Position(path: [0, 0], offset: 1),
end: Position(path: [0, 1], offset: 1),
);
expect(
cubit.calculateTurnIntoSelection(
editorState.getNodeAtPath([0])!,
selection5,
),
Selection.collapsed(Position(path: [0])),
);
// case 6, not collapsed selection and the selection is in the nested level
// and tap the turn into button at the [0, 0]
final selection6 = Selection(
start: Position(path: [0, 0], offset: 1),
end: Position(path: [0, 1], offset: 1),
);
expect(
cubit.calculateTurnIntoSelection(
editorState.getNodeAtPath([0])!,
selection6,
),
Selection.collapsed(Position(path: [0])),
);
// case 7, multiple blocks selection, and tap the turn into button of one of the selected nodes
final selection7 = Selection(
start: Position(path: [0], offset: 1),
end: Position(path: [2], offset: 1),
);
expect(
cubit.calculateTurnIntoSelection(
editorState.getNodeAtPath([1])!,
selection7,
),
selection7,
);
// case 8, multiple blocks selection, and tap the turn into button of one of the non-selected nodes
final selection8 = Selection(
start: Position(path: [0], offset: 1),
end: Position(path: [1], offset: 1),
);
expect(
cubit.calculateTurnIntoSelection(
editorState.getNodeAtPath([2])!,
selection8,
),
Selection.collapsed(Position(path: [2])),
);
});
});
}

View File

@ -114,7 +114,7 @@
"html": "HTML",
"clipboard": "Copy to clipboard",
"csv": "CSV",
"copyLink": "Copy Link",
"copyLink": "Copy link",
"publishToTheWeb": "Publish to Web",
"publishToTheWebHint": "Create a website with AppFlowy",
"publish": "Publish",
@ -162,7 +162,7 @@
"openNewTab": "Open in a new tab",
"moveTo": "Move to",
"addToFavorites": "Add to Favorites",
"copyLink": "Copy Link",
"copyLink": "Copy link",
"changeIcon": "Change icon",
"collapseAllPages": "Collapse all subpages"
},
@ -2713,4 +2713,4 @@
"refreshNote": "After successful upgrade, click <refresh/> to activate your new features.",
"refresh": "here"
}
}
}