feat: support multiple blocks operation (#6958)

* feat: support multiple blocks operation

* test: support multiple blocks operation
This commit is contained in:
Lucas 2024-12-11 16:37:07 +08:00 committed by GitHub
parent d68212f4ce
commit 8b672a159f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 152 additions and 22 deletions

View File

@ -138,13 +138,6 @@ void main() {
);
});
testWidgets('insert a jpg image from network', (tester) async {
await testEmbedImage(
tester,
'https://file-examples.com/storage/fe9566cb7d67345489a5a97/2017/10/file_example_JPG_100kB.jpg',
);
});
testWidgets('insert a bmp image from network', (tester) async {
await testEmbedImage(
tester,

View File

@ -28,12 +28,11 @@ class BlockActionOptionCubit extends Cubit<BlockActionOptionState> {
final transaction = editorState.transaction;
switch (action) {
case OptionAction.delete:
transaction.deleteNode(node);
_deleteBlocks(transaction, node);
break;
case OptionAction.duplicate:
await _duplicateBlock(transaction, node);
EditorNotification.paste().post();
break;
case OptionAction.moveUp:
transaction.moveNode(node.path.previous, node);
@ -55,9 +54,40 @@ class BlockActionOptionCubit extends Cubit<BlockActionOptionState> {
await editorState.apply(transaction);
}
/// If the selection is a block selection, delete the selected blocks.
/// Otherwise, delete the selected block.
void _deleteBlocks(Transaction transaction, Node selectedNode) {
final selection = editorState.selection;
final selectionType = editorState.selectionType;
if (selectionType == SelectionType.block && selection != null) {
final nodes = editorState.getNodesInSelection(selection.normalized);
transaction.deleteNodes(nodes);
} else {
transaction.deleteNode(selectedNode);
}
}
Future<void> _duplicateBlock(Transaction transaction, Node node) async {
final selection = editorState.selection;
final selectionType = editorState.selectionType;
if (selectionType == SelectionType.block && selection != null) {
final nodes = editorState.getNodesInSelection(selection.normalized);
for (final node in nodes) {
_validateNode(node);
}
transaction.insertNodes(
selection.normalized.end.path.next,
nodes.map((e) => _copyBlock(e)).toList(),
);
} else {
_validateNode(node);
transaction.insertNode(node.path.next, _copyBlock(node));
}
}
void _validateNode(Node node) {
final type = node.type;
final builder = editorState.renderer.blockComponentBuilder(type);
final builder = blockComponentBuilder[type];
if (builder == null) {
Log.error('Block type $type is not supported');
@ -68,15 +98,13 @@ class BlockActionOptionCubit extends Cubit<BlockActionOptionState> {
if (!valid) {
Log.error('Block type $type is not valid');
}
transaction.insertNode(node.path.next, _copyBlock(node));
}
Node _copyBlock(Node node) {
Node copiedNode = node.copyWith();
final type = node.type;
final builder = editorState.renderer.blockComponentBuilder(type);
final builder = blockComponentBuilder[type];
if (builder == null) {
Log.error('Block type $type is not supported');
@ -182,6 +210,14 @@ class BlockActionOptionCubit extends Cubit<BlockActionOptionState> {
}
Future<void> _copyLinkToBlock(Node node) async {
List<Node> nodes = [node];
final selection = editorState.selection;
final selectionType = editorState.selectionType;
if (selectionType == SelectionType.block && selection != null) {
nodes = editorState.getNodesInSelection(selection.normalized);
}
final context = editorState.document.root.context;
final viewId = context?.read<DocumentBloc>().documentId;
if (viewId == null) {
@ -200,13 +236,17 @@ class BlockActionOptionCubit extends Cubit<BlockActionOptionState> {
return;
}
final link = ShareConstants.buildShareUrl(
workspaceId: workspaceId,
viewId: viewId,
blockId: node.id,
final blockIds = nodes.map((e) => e.id);
final links = blockIds.map(
(e) => ShareConstants.buildShareUrl(
workspaceId: workspaceId,
viewId: viewId,
blockId: e,
),
);
await getIt<ClipboardService>().setData(
ClipboardServiceData(plainText: link),
ClipboardServiceData(plainText: links.join('\n')),
);
emit(BlockActionOptionState()); // Emit a new state to trigger UI update

View File

@ -25,6 +25,7 @@ class DraggableOptionButton extends StatefulWidget {
final EditorState editorState;
final BlockComponentContext blockComponentContext;
final Map<String, BlockComponentBuilder> blockComponentBuilder;
@override
State<DraggableOptionButton> createState() => _DraggableOptionButtonState();
}

View File

@ -147,10 +147,25 @@ class _ColorOptionButtonState extends State<ColorOptionButton> {
color: AFThemeExtension.of(context).onBackground,
),
onTap: (option, index) async {
final transaction = widget.editorState.transaction;
transaction.updateNode(node, {
blockComponentBackgroundColor: option.id,
});
final editorState = widget.editorState;
final transaction = editorState.transaction;
final selectionType = editorState.selectionType;
final selection = editorState.selection;
// In multiple selection, we need to update all the nodes in the selection
if (selectionType == SelectionType.block && selection != null) {
final nodes = editorState.getNodesInSelection(selection.normalized);
for (final node in nodes) {
transaction.updateNode(node, {
blockComponentBackgroundColor: option.id,
});
}
} else {
transaction.updateNode(node, {
blockComponentBackgroundColor: option.id,
});
}
await widget.editorState.apply(transaction);
innerController.close();

View File

@ -0,0 +1,81 @@
import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/block_action_option_cubit.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
group('block action option cubit:', () {
setUpAll(() {
Log.shared.disableLog = true;
});
tearDownAll(() {
Log.shared.disableLog = false;
});
test('delete blocks', () async {
const text = 'paragraph';
final document = Document.blank()
..insert([
0,
], [
paragraphNode(text: text),
paragraphNode(text: text),
paragraphNode(text: text),
]);
final editorState = EditorState(document: document);
final cubit = BlockActionOptionCubit(
editorState: editorState,
blockComponentBuilder: {},
);
editorState.selection = Selection(
start: Position(path: [0]),
end: Position(path: [2], offset: text.length),
);
editorState.selectionType = SelectionType.block;
await cubit.handleAction(OptionAction.delete, document.nodeAtPath([0])!);
// all the nodes should be deleted
expect(document.root.children, isEmpty);
editorState.dispose();
});
test('duplicate blocks', () async {
const text = 'paragraph';
final document = Document.blank()
..insert([
0,
], [
paragraphNode(text: text),
paragraphNode(text: text),
paragraphNode(text: text),
]);
final editorState = EditorState(document: document);
final cubit = BlockActionOptionCubit(
editorState: editorState,
blockComponentBuilder: {},
);
editorState.selection = Selection(
start: Position(path: [0]),
end: Position(path: [2], offset: text.length),
);
editorState.selectionType = SelectionType.block;
await cubit.handleAction(
OptionAction.duplicate,
document.nodeAtPath([0])!,
);
expect(document.root.children, hasLength(6));
editorState.dispose();
});
});
}