Merge pull request #749 from AppFlowy-IO/feat/transaction-to-json

Feat: transaction to json
This commit is contained in:
Vincent Chan 2022-08-01 17:20:56 +08:00 committed by GitHub
commit 9b764731e7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 196 additions and 65 deletions

View File

@ -34,4 +34,11 @@ class Position {
@override @override
String toString() => 'path = $path, offset = $offset'; String toString() => 'path = $path, offset = $offset';
Map<String, dynamic> toJson() {
return {
"path": path.toList(),
"offset": offset,
};
}
} }

View File

@ -48,4 +48,11 @@ class Selection {
@override @override
String toString() => '[Selection] start = $start, end = $end'; String toString() => '[Selection] start = $start, end = $end';
Map<String, dynamic> toJson() {
return {
"start": start.toJson(),
"end": end.toJson(),
};
}
} }

View File

@ -2,22 +2,40 @@ import 'package:flowy_editor/document/attributes.dart';
import 'package:flowy_editor/flowy_editor.dart'; import 'package:flowy_editor/flowy_editor.dart';
abstract class Operation { abstract class Operation {
factory Operation.fromJson(Map<String, dynamic> map) {
String t = map["type"] as String;
if (t == "insert-operation") {
return InsertOperation.fromJson(map);
} else if (t == "update-operation") {
return UpdateOperation.fromJson(map);
} else if (t == "delete-operation") {
return DeleteOperation.fromJson(map);
} else if (t == "text-edit-operation") {
return TextEditOperation.fromJson(map);
}
throw ArgumentError('unexpected type $t');
}
final Path path; final Path path;
Operation({required this.path}); Operation(this.path);
Operation copyWithPath(Path path); Operation copyWithPath(Path path);
Operation invert(); Operation invert();
Map<String, dynamic> toJson();
} }
class InsertOperation extends Operation { class InsertOperation extends Operation {
final Node value; final Node value;
InsertOperation({ factory InsertOperation.fromJson(Map<String, dynamic> map) {
required super.path, final path = map["path"] as List<int>;
required this.value, final value = Node.fromJson(map["value"]);
}); return InsertOperation(path, value);
}
InsertOperation(Path path, this.value) : super(path);
InsertOperation copyWith({Path? path, Node? value}) => InsertOperation copyWith({Path? path, Node? value}) =>
InsertOperation(path: path ?? this.path, value: value ?? this.value); InsertOperation(path ?? this.path, value ?? this.value);
@override @override
Operation copyWithPath(Path path) => copyWith(path: path); Operation copyWithPath(Path path) => copyWith(path: path);
@ -25,28 +43,42 @@ class InsertOperation extends Operation {
@override @override
Operation invert() { Operation invert() {
return DeleteOperation( return DeleteOperation(
path: path, path,
removedValue: value, value,
); );
} }
@override
Map<String, dynamic> toJson() {
return {
"type": "insert-operation",
"path": path.toList(),
"value": value.toJson(),
};
}
} }
class UpdateOperation extends Operation { class UpdateOperation extends Operation {
final Attributes attributes; final Attributes attributes;
final Attributes oldAttributes; final Attributes oldAttributes;
UpdateOperation({ factory UpdateOperation.fromJson(Map<String, dynamic> map) {
required super.path, final path = map["path"] as List<int>;
required this.attributes, final attributes = map["attributes"] as Map<String, dynamic>;
required this.oldAttributes, final oldAttributes = map["oldAttributes"] as Map<String, dynamic>;
}); return UpdateOperation(path, attributes, oldAttributes);
}
UpdateOperation(
Path path,
this.attributes,
this.oldAttributes,
) : super(path);
UpdateOperation copyWith( UpdateOperation copyWith(
{Path? path, Attributes? attributes, Attributes? oldAttributes}) => {Path? path, Attributes? attributes, Attributes? oldAttributes}) =>
UpdateOperation( UpdateOperation(path ?? this.path, attributes ?? this.attributes,
path: path ?? this.path, oldAttributes ?? this.oldAttributes);
attributes: attributes ?? this.attributes,
oldAttributes: oldAttributes ?? this.oldAttributes);
@override @override
Operation copyWithPath(Path path) => copyWith(path: path); Operation copyWithPath(Path path) => copyWith(path: path);
@ -54,33 +86,55 @@ class UpdateOperation extends Operation {
@override @override
Operation invert() { Operation invert() {
return UpdateOperation( return UpdateOperation(
path: path, path,
attributes: oldAttributes, oldAttributes,
oldAttributes: attributes, attributes,
); );
} }
@override
Map<String, dynamic> toJson() {
return {
"type": "update-operation",
"path": path.toList(),
"attributes": {...attributes},
"oldAttributes": {...oldAttributes},
};
}
} }
class DeleteOperation extends Operation { class DeleteOperation extends Operation {
final Node removedValue; final Node removedValue;
DeleteOperation({ factory DeleteOperation.fromJson(Map<String, dynamic> map) {
required super.path, final path = map["path"] as List<int>;
required this.removedValue, final removedValue = Node.fromJson(map["removedValue"]);
}); return DeleteOperation(path, removedValue);
}
DeleteOperation copyWith({Path? path, Node? removedValue}) => DeleteOperation( DeleteOperation(
path: path ?? this.path, removedValue: removedValue ?? this.removedValue); Path path,
this.removedValue,
) : super(path);
DeleteOperation copyWith({Path? path, Node? removedValue}) =>
DeleteOperation(path ?? this.path, removedValue ?? this.removedValue);
@override @override
Operation copyWithPath(Path path) => copyWith(path: path); Operation copyWithPath(Path path) => copyWith(path: path);
@override @override
Operation invert() { Operation invert() {
return InsertOperation( return InsertOperation(path, removedValue);
path: path, }
value: removedValue,
); @override
Map<String, dynamic> toJson() {
return {
"type": "delete-operation",
"path": path.toList(),
"removedValue": removedValue.toJson(),
};
} }
} }
@ -88,24 +142,39 @@ class TextEditOperation extends Operation {
final Delta delta; final Delta delta;
final Delta inverted; final Delta inverted;
TextEditOperation({ factory TextEditOperation.fromJson(Map<String, dynamic> map) {
required super.path, final path = map["path"] as List<int>;
required this.delta, final delta = Delta.fromJson(map["delta"]);
required this.inverted, final invert = Delta.fromJson(map["invert"]);
}); return TextEditOperation(path, delta, invert);
}
TextEditOperation(
Path path,
this.delta,
this.inverted,
) : super(path);
TextEditOperation copyWith({Path? path, Delta? delta, Delta? inverted}) => TextEditOperation copyWith({Path? path, Delta? delta, Delta? inverted}) =>
TextEditOperation( TextEditOperation(
path: path ?? this.path, path ?? this.path, delta ?? this.delta, inverted ?? this.inverted);
delta: delta ?? this.delta,
inverted: inverted ?? this.inverted);
@override @override
Operation copyWithPath(Path path) => copyWith(path: path); Operation copyWithPath(Path path) => copyWith(path: path);
@override @override
Operation invert() { Operation invert() {
return TextEditOperation(path: path, delta: inverted, inverted: delta); return TextEditOperation(path, inverted, delta);
}
@override
Map<String, dynamic> toJson() {
return {
"type": "text-edit-operation",
"path": path.toList(),
"delta": delta.toJson(),
"invert": inverted.toJson(),
};
} }
} }

View File

@ -23,4 +23,17 @@ class Transaction {
this.beforeSelection, this.beforeSelection,
this.afterSelection, this.afterSelection,
}); });
Map<String, dynamic> toJson() {
final Map<String, dynamic> result = {
"operations": operations.map((e) => e.toJson()),
};
if (beforeSelection != null) {
result["beforeSelection"] = beforeSelection!.toJson();
}
if (afterSelection != null) {
result["afterSelection"] = afterSelection!.toJson();
}
return result;
}
} }

View File

@ -1,5 +1,4 @@
import 'dart:collection'; import 'dart:collection';
import 'dart:math';
import 'package:flowy_editor/editor_state.dart'; import 'package:flowy_editor/editor_state.dart';
import 'package:flowy_editor/document/node.dart'; import 'package:flowy_editor/document/node.dart';
import 'package:flowy_editor/document/path.dart'; import 'package:flowy_editor/document/path.dart';
@ -31,21 +30,21 @@ class TransactionBuilder {
insertNode(Path path, Node node) { insertNode(Path path, Node node) {
beforeSelection = state.cursorSelection; beforeSelection = state.cursorSelection;
add(InsertOperation(path: path, value: node)); add(InsertOperation(path, node));
} }
updateNode(Node node, Attributes attributes) { updateNode(Node node, Attributes attributes) {
beforeSelection = state.cursorSelection; beforeSelection = state.cursorSelection;
add(UpdateOperation( add(UpdateOperation(
path: node.path, node.path,
attributes: Attributes.from(node.attributes)..addAll(attributes), Attributes.from(node.attributes)..addAll(attributes),
oldAttributes: node.attributes, node.attributes,
)); ));
} }
deleteNode(Node node) { deleteNode(Node node) {
beforeSelection = state.cursorSelection; beforeSelection = state.cursorSelection;
add(DeleteOperation(path: node.path, removedValue: node)); add(DeleteOperation(node.path, node));
} }
deleteNodes(List<Node> nodes) { deleteNodes(List<Node> nodes) {
@ -60,7 +59,7 @@ class TransactionBuilder {
final inverted = delta.invert(node.delta); final inverted = delta.invert(node.delta);
add(TextEditOperation(path: path, delta: delta, inverted: inverted)); add(TextEditOperation(path, delta, inverted));
} }
mergeText(TextNode firstNode, TextNode secondNode, mergeText(TextNode firstNode, TextNode secondNode,
@ -119,9 +118,9 @@ class TransactionBuilder {
last is TextEditOperation && last is TextEditOperation &&
pathEquals(op.path, last.path)) { pathEquals(op.path, last.path)) {
final newOp = TextEditOperation( final newOp = TextEditOperation(
path: op.path, op.path,
delta: last.delta.compose(op.delta), last.delta.compose(op.delta),
inverted: op.inverted.compose(last.inverted), op.inverted.compose(last.inverted),
); );
operations[operations.length - 1] = newOp; operations[operations.length - 1] = newOp;
return; return;

View File

@ -27,26 +27,18 @@ void main() {
group('transform operation', () { group('transform operation', () {
test('insert + insert', () { test('insert + insert', () {
final t = transformOperation( final t = transformOperation(
InsertOperation(path: [ InsertOperation([0, 1],
0, Node(type: "node", attributes: {}, children: LinkedList())),
1 InsertOperation([0, 1],
], value: Node(type: "node", attributes: {}, children: LinkedList())), Node(type: "node", attributes: {}, children: LinkedList())));
InsertOperation(
path: [0, 1],
value:
Node(type: "node", attributes: {}, children: LinkedList())));
expect(t.path, [0, 2]); expect(t.path, [0, 2]);
}); });
test('delete + delete', () { test('delete + delete', () {
final t = transformOperation( final t = transformOperation(
DeleteOperation( DeleteOperation([0, 1],
path: [0, 1], Node(type: "node", attributes: {}, children: LinkedList())),
removedValue: DeleteOperation([0, 2],
Node(type: "node", attributes: {}, children: LinkedList())), Node(type: "node", attributes: {}, children: LinkedList())));
DeleteOperation(
path: [0, 2],
removedValue:
Node(type: "node", attributes: {}, children: LinkedList())));
expect(t.path, [0, 1]); expect(t.path, [0, 1]);
}); });
}); });
@ -78,4 +70,48 @@ void main() {
expect(transaction.operations[1].path, [0]); expect(transaction.operations[1].path, [0]);
expect(transaction.operations[2].path, [0]); expect(transaction.operations[2].path, [0]);
}); });
group("toJson", () {
test("insert", () {
final root = Node(type: "root", attributes: {}, children: LinkedList());
final state = EditorState(document: StateTree(root: root));
final item1 = Node(type: "node", attributes: {}, children: LinkedList());
final tb = TransactionBuilder(state);
tb.insertNode([0], item1);
final transaction = tb.finish();
expect(transaction.toJson(), {
"operations": [
{
"type": "insert-operation",
"path": [0],
"value": item1.toJson(),
}
],
});
});
test("delete", () {
final item1 = Node(type: "node", attributes: {}, children: LinkedList());
final root = Node(
type: "root",
attributes: {},
children: LinkedList()
..addAll([
item1,
]));
final state = EditorState(document: StateTree(root: root));
final tb = TransactionBuilder(state);
tb.deleteNode(item1);
final transaction = tb.finish();
expect(transaction.toJson(), {
"operations": [
{
"type": "delete-operation",
"path": [0],
"removedValue": item1.toJson(),
}
],
});
});
});
} }