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
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
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';
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;
Operation({required this.path});
Operation(this.path);
Operation copyWithPath(Path path);
Operation invert();
Map<String, dynamic> toJson();
}
class InsertOperation extends Operation {
final Node value;
InsertOperation({
required super.path,
required this.value,
});
factory InsertOperation.fromJson(Map<String, dynamic> map) {
final path = map["path"] as List<int>;
final value = Node.fromJson(map["value"]);
return InsertOperation(path, value);
}
InsertOperation(Path path, this.value) : super(path);
InsertOperation copyWith({Path? path, Node? value}) =>
InsertOperation(path: path ?? this.path, value: value ?? this.value);
InsertOperation(path ?? this.path, value ?? this.value);
@override
Operation copyWithPath(Path path) => copyWith(path: path);
@ -25,28 +43,42 @@ class InsertOperation extends Operation {
@override
Operation invert() {
return DeleteOperation(
path: path,
removedValue: value,
path,
value,
);
}
@override
Map<String, dynamic> toJson() {
return {
"type": "insert-operation",
"path": path.toList(),
"value": value.toJson(),
};
}
}
class UpdateOperation extends Operation {
final Attributes attributes;
final Attributes oldAttributes;
UpdateOperation({
required super.path,
required this.attributes,
required this.oldAttributes,
});
factory UpdateOperation.fromJson(Map<String, dynamic> map) {
final path = map["path"] as List<int>;
final attributes = map["attributes"] as Map<String, dynamic>;
final oldAttributes = map["oldAttributes"] as Map<String, dynamic>;
return UpdateOperation(path, attributes, oldAttributes);
}
UpdateOperation(
Path path,
this.attributes,
this.oldAttributes,
) : super(path);
UpdateOperation copyWith(
{Path? path, Attributes? attributes, Attributes? oldAttributes}) =>
UpdateOperation(
path: path ?? this.path,
attributes: attributes ?? this.attributes,
oldAttributes: oldAttributes ?? this.oldAttributes);
UpdateOperation(path ?? this.path, attributes ?? this.attributes,
oldAttributes ?? this.oldAttributes);
@override
Operation copyWithPath(Path path) => copyWith(path: path);
@ -54,33 +86,55 @@ class UpdateOperation extends Operation {
@override
Operation invert() {
return UpdateOperation(
path: path,
attributes: oldAttributes,
oldAttributes: attributes,
path,
oldAttributes,
attributes,
);
}
@override
Map<String, dynamic> toJson() {
return {
"type": "update-operation",
"path": path.toList(),
"attributes": {...attributes},
"oldAttributes": {...oldAttributes},
};
}
}
class DeleteOperation extends Operation {
final Node removedValue;
DeleteOperation({
required super.path,
required this.removedValue,
});
factory DeleteOperation.fromJson(Map<String, dynamic> map) {
final path = map["path"] as List<int>;
final removedValue = Node.fromJson(map["removedValue"]);
return DeleteOperation(path, removedValue);
}
DeleteOperation copyWith({Path? path, Node? removedValue}) => DeleteOperation(
path: path ?? this.path, removedValue: removedValue ?? this.removedValue);
DeleteOperation(
Path path,
this.removedValue,
) : super(path);
DeleteOperation copyWith({Path? path, Node? removedValue}) =>
DeleteOperation(path ?? this.path, removedValue ?? this.removedValue);
@override
Operation copyWithPath(Path path) => copyWith(path: path);
@override
Operation invert() {
return InsertOperation(
path: path,
value: removedValue,
);
return InsertOperation(path, 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 inverted;
TextEditOperation({
required super.path,
required this.delta,
required this.inverted,
});
factory TextEditOperation.fromJson(Map<String, dynamic> map) {
final path = map["path"] as List<int>;
final delta = Delta.fromJson(map["delta"]);
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(
path: path ?? this.path,
delta: delta ?? this.delta,
inverted: inverted ?? this.inverted);
path ?? this.path, delta ?? this.delta, inverted ?? this.inverted);
@override
Operation copyWithPath(Path path) => copyWith(path: path);
@override
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.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:math';
import 'package:flowy_editor/editor_state.dart';
import 'package:flowy_editor/document/node.dart';
import 'package:flowy_editor/document/path.dart';
@ -31,21 +30,21 @@ class TransactionBuilder {
insertNode(Path path, Node node) {
beforeSelection = state.cursorSelection;
add(InsertOperation(path: path, value: node));
add(InsertOperation(path, node));
}
updateNode(Node node, Attributes attributes) {
beforeSelection = state.cursorSelection;
add(UpdateOperation(
path: node.path,
attributes: Attributes.from(node.attributes)..addAll(attributes),
oldAttributes: node.attributes,
node.path,
Attributes.from(node.attributes)..addAll(attributes),
node.attributes,
));
}
deleteNode(Node node) {
beforeSelection = state.cursorSelection;
add(DeleteOperation(path: node.path, removedValue: node));
add(DeleteOperation(node.path, node));
}
deleteNodes(List<Node> nodes) {
@ -60,7 +59,7 @@ class TransactionBuilder {
final inverted = delta.invert(node.delta);
add(TextEditOperation(path: path, delta: delta, inverted: inverted));
add(TextEditOperation(path, delta, inverted));
}
mergeText(TextNode firstNode, TextNode secondNode,
@ -119,9 +118,9 @@ class TransactionBuilder {
last is TextEditOperation &&
pathEquals(op.path, last.path)) {
final newOp = TextEditOperation(
path: op.path,
delta: last.delta.compose(op.delta),
inverted: op.inverted.compose(last.inverted),
op.path,
last.delta.compose(op.delta),
op.inverted.compose(last.inverted),
);
operations[operations.length - 1] = newOp;
return;

View File

@ -27,26 +27,18 @@ void main() {
group('transform operation', () {
test('insert + insert', () {
final t = transformOperation(
InsertOperation(path: [
0,
1
], value: Node(type: "node", attributes: {}, children: LinkedList())),
InsertOperation(
path: [0, 1],
value:
Node(type: "node", attributes: {}, children: LinkedList())));
InsertOperation([0, 1],
Node(type: "node", attributes: {}, children: LinkedList())),
InsertOperation([0, 1],
Node(type: "node", attributes: {}, children: LinkedList())));
expect(t.path, [0, 2]);
});
test('delete + delete', () {
final t = transformOperation(
DeleteOperation(
path: [0, 1],
removedValue:
Node(type: "node", attributes: {}, children: LinkedList())),
DeleteOperation(
path: [0, 2],
removedValue:
Node(type: "node", attributes: {}, children: LinkedList())));
DeleteOperation([0, 1],
Node(type: "node", attributes: {}, children: LinkedList())),
DeleteOperation([0, 2],
Node(type: "node", attributes: {}, children: LinkedList())));
expect(t.path, [0, 1]);
});
});
@ -78,4 +70,48 @@ void main() {
expect(transaction.operations[1].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(),
}
],
});
});
});
}