Procházet zdrojové kódy

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

Feat: transaction to json
Vincent Chan před 2 roky
rodič
revize
9b764731e7

+ 7 - 0
frontend/app_flowy/packages/flowy_editor/lib/document/position.dart

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

+ 7 - 0
frontend/app_flowy/packages/flowy_editor/lib/document/selection.dart

@@ -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(),
+    };
+  }
 }

+ 108 - 39
frontend/app_flowy/packages/flowy_editor/lib/operation/operation.dart

@@ -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(
+    Path path,
+    this.removedValue,
+  ) : super(path);
 
-  DeleteOperation copyWith({Path? path, Node? removedValue}) => DeleteOperation(
-      path: path ?? this.path, removedValue: removedValue ?? this.removedValue);
+  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(),
+    };
   }
 }
 

+ 13 - 0
frontend/app_flowy/packages/flowy_editor/lib/operation/transaction.dart

@@ -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;
+  }
 }

+ 9 - 10
frontend/app_flowy/packages/flowy_editor/lib/operation/transaction_builder.dart

@@ -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;

+ 52 - 16
frontend/app_flowy/packages/flowy_editor/test/operation_test.dart

@@ -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(),
+          }
+        ],
+      });
+    });
+  });
 }