mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2025-12-28 23:58:28 +00:00
feat: support multiple blocks operation (#6958)
* feat: support multiple blocks operation * test: support multiple blocks operation
This commit is contained in:
parent
d68212f4ce
commit
8b672a159f
@ -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,
|
||||
|
||||
@ -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
|
||||
|
||||
@ -25,6 +25,7 @@ class DraggableOptionButton extends StatefulWidget {
|
||||
final EditorState editorState;
|
||||
final BlockComponentContext blockComponentContext;
|
||||
final Map<String, BlockComponentBuilder> blockComponentBuilder;
|
||||
|
||||
@override
|
||||
State<DraggableOptionButton> createState() => _DraggableOptionButtonState();
|
||||
}
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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();
|
||||
});
|
||||
});
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user