瀏覽代碼

feat: document migration from 0.1.x to 0.2.0 (#2583)

* chore: migrate the rewrite feature

* chore: rename flowy-document

* feat: add initial_data interface

* chore: rename the document event

* fix: font name error

* fix: export page UI issues

* feat: implement editor migration 0.1.x -> 0.2.0

* feat: support import old json

* fix: nested list error

* chore: update pubspec
Lucas.Xu 2 年之前
父節點
當前提交
ffff628359
共有 26 個文件被更改,包括 756 次插入159 次删除
  1. 254 0
      frontend/appflowy_flutter/assets/template/readme.json
  2. 9 3
      frontend/appflowy_flutter/lib/plugins/document/application/doc_bloc.dart
  3. 9 9
      frontend/appflowy_flutter/lib/plugins/document/application/doc_service.dart
  4. 19 2
      frontend/appflowy_flutter/lib/plugins/document/application/document_data_pb_extension.dart
  5. 11 3
      frontend/appflowy_flutter/lib/plugins/document/application/editor_transaction_adapter.dart
  6. 3 2
      frontend/appflowy_flutter/lib/plugins/document/document_page.dart
  7. 1 0
      frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart
  8. 2 0
      frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/emoji_picker_button.dart
  9. 6 5
      frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/callout/callout_block_component.dart
  10. 4 4
      frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/cover/change_cover_popover.dart
  11. 5 4
      frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/cover/change_cover_popover_bloc.dart
  12. 46 40
      frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/cover/cover_node_widget.dart
  13. 154 0
      frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/migration/editor_migration.dart
  14. 130 4
      frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/openai/widgets/auto_completion_node_widget.dart
  15. 0 4
      frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/openai/widgets/smart_edit_node_widget.dart
  16. 4 7
      frontend/appflowy_flutter/lib/plugins/document/presentation/editor_style.dart
  17. 11 14
      frontend/appflowy_flutter/lib/plugins/document/presentation/export_page_widget.dart
  18. 2 2
      frontend/appflowy_flutter/pubspec.lock
  19. 2 1
      frontend/appflowy_flutter/pubspec.yaml
  20. 18 18
      frontend/appflowy_tauri/src/appflowy_app/stores/effects/document/document_bd_svc.ts
  21. 1 0
      frontend/rust-lib/Cargo.lock
  22. 1 0
      frontend/rust-lib/flowy-document2/Cargo.toml
  23. 27 2
      frontend/rust-lib/flowy-document2/src/document_data.rs
  24. 9 9
      frontend/rust-lib/flowy-document2/src/entities.rs
  25. 17 15
      frontend/rust-lib/flowy-document2/src/event_handler.rs
  26. 11 11
      frontend/rust-lib/flowy-document2/src/event_map.rs

+ 254 - 0
frontend/appflowy_flutter/assets/template/readme.json

@@ -0,0 +1,254 @@
+{
+    "document": {
+      "type": "editor",
+      "children": [
+        { "type": "cover" },
+        {
+          "type": "text",
+          "attributes": { "subtype": "heading", "heading": "h1" },
+          "delta": [{ "insert": "Welcome to AppFlowy!" }]
+        },
+        {
+          "type": "text",
+          "attributes": { "subtype": "heading", "heading": "h2" },
+          "delta": [{ "insert": "Here are the basics" }]
+        },
+        {
+          "type": "text",
+          "attributes": { "subtype": "checkbox", "checkbox": null },
+          "delta": [{ "insert": "Click anywhere and just start typing." }]
+        },
+        {
+          "type": "text",
+          "attributes": { "subtype": "checkbox", "checkbox": false },
+          "delta": [
+            {
+              "insert": "Highlight ",
+              "attributes": { "backgroundColor": "0x4dffeb3b" }
+            },
+            { "insert": "any text, and use the editing menu to " },
+            { "insert": "style", "attributes": { "italic": true } },
+            { "insert": " " },
+            { "insert": "your", "attributes": { "bold": true } },
+            { "insert": " " },
+            { "insert": "writing", "attributes": { "underline": true } },
+            { "insert": " " },
+            { "insert": "however", "attributes": { "code": true } },
+            { "insert": " you " },
+            { "insert": "like.", "attributes": { "strikethrough": true } }
+          ]
+        },
+        {
+          "type": "text",
+          "attributes": { "subtype": "checkbox", "checkbox": null },
+          "delta": [
+            { "insert": "As soon as you type " },
+            {
+              "insert": "/",
+              "attributes": { "code": true, "color": "0xff00b5ff" }
+            },
+            { "insert": " a menu will pop up. Select " },
+            {
+              "insert": "different types",
+              "attributes": { "backgroundColor": "0x4d9c27b0" }
+            },
+            { "insert": " of content blocks you can add." }
+          ]
+        },
+        {
+          "type": "text",
+          "attributes": { "subtype": "checkbox", "checkbox": null },
+          "delta": [
+            { "insert": "Type " },
+            { "insert": "/", "attributes": { "code": true } },
+            { "insert": " followed by " },
+            { "insert": "/bullet", "attributes": { "code": true } },
+            { "insert": " or " },
+            { "insert": "/num", "attributes": { "code": true } },
+            { "insert": " to create a list.", "attributes": { "code": false } }
+          ]
+        },
+        {
+          "type": "text",
+          "attributes": { "subtype": "checkbox", "checkbox": true },
+          "delta": [
+            { "insert": "Click " },
+            { "insert": "+ New Page ", "attributes": { "code": true } },
+            {
+              "insert": "button at the bottom of your sidebar to add a new page."
+            }
+          ]
+        },
+        {
+          "type": "text",
+          "attributes": { "subtype": "checkbox", "checkbox": null },
+          "delta": [
+            { "insert": "Click " },
+            { "insert": "+", "attributes": { "code": true } },
+            { "insert": " next to any page title in the sidebar to " },
+            { "insert": "quickly", "attributes": { "color": "0xff8427e0" } },
+            { "insert": " add a new subpage, " },
+            { "insert": "Document", "attributes": { "code": true } },
+            { "insert": ", ", "attributes": { "code": false } },
+            { "insert": "Grid", "attributes": { "code": true } },
+            { "insert": ", or ", "attributes": { "code": false } },
+            { "insert": "Kanban Board", "attributes": { "code": true } },
+            { "insert": ".", "attributes": { "code": false } }
+          ]
+        },
+        { "type": "text", "delta": [] },
+        { "type": "divider" },
+        { "type": "text", "attributes": { "checkbox": null }, "delta": [] },
+        {
+          "type": "text",
+          "attributes": {
+            "subtype": "heading",
+            "checkbox": null,
+            "heading": "h2"
+          },
+          "delta": [{ "insert": "Keyboard shortcuts, markdown, and code block" }]
+        },
+        {
+          "type": "text",
+          "attributes": {
+            "subtype": "number-list",
+            "number": 1,
+            "heading": null
+          },
+          "delta": [
+            { "insert": "Keyboard shortcuts " },
+            {
+              "insert": "guide",
+              "attributes": {
+                "href": "https://appflowy.gitbook.io/docs/essential-documentation/shortcuts"
+              }
+            },
+            { "retain": 1, "attributes": { "strikethrough": true } }
+          ]
+        },
+        {
+          "type": "text",
+          "attributes": {
+            "subtype": "number-list",
+            "number": 2,
+            "heading": null
+          },
+          "delta": [
+            { "insert": "Markdown " },
+            {
+              "insert": "reference",
+              "attributes": {
+                "href": "https://appflowy.gitbook.io/docs/essential-documentation/markdown"
+              }
+            },
+            { "retain": 1, "attributes": { "strikethrough": true } }
+          ]
+        },
+        {
+          "type": "text",
+          "attributes": { "number": 3, "subtype": "number-list" },
+          "delta": [
+            { "insert": "Type " },
+            { "insert": "/code", "attributes": { "code": true } },
+            {
+              "insert": " to insert a code block",
+              "attributes": { "code": false }
+            }
+          ]
+        },
+        {
+          "type": "text",
+          "attributes": {
+            "subtype": "code_block",
+            "number": 3,
+            "heading": null,
+            "number-list": null,
+            "theme": "vs",
+            "language": "rust"
+          },
+          "delta": [
+            {
+              "insert": "// This is the main function.\nfn main() {\n    // Print text to the console.\n    println!(\"Hello World!\");\n}"
+            },
+            { "retain": 1, "attributes": { "strikethrough": true } }
+          ]
+        },
+        { "type": "text", "attributes": { "checkbox": null }, "delta": [] },
+        {
+          "type": "text",
+          "attributes": {
+            "subtype": "heading",
+            "checkbox": null,
+            "heading": "h2"
+          },
+          "delta": [{ "insert": "Have a question❓" }]
+        },
+        {
+          "type": "text",
+          "attributes": { "subtype": "quote" },
+          "delta": [
+            { "insert": "Click " },
+            { "insert": "?", "attributes": { "code": true } },
+            { "insert": " at the bottom right for help and support." }
+          ]
+        },
+        { "type": "text", "delta": [] },
+        {
+          "type": "callout",
+          "children": [
+            { "type": "text", "delta": [] },
+            {
+              "type": "text",
+              "attributes": { "subtype": "heading", "heading": "h2" },
+              "delta": [{ "insert": "Like AppFlowy? Follow us:" }]
+            },
+            {
+              "type": "text",
+              "attributes": { "subtype": "bulleted-list" },
+              "delta": [
+                {
+                  "insert": "GitHub",
+                  "attributes": {
+                    "href": "https://github.com/AppFlowy-IO/AppFlowy"
+                  }
+                }
+              ]
+            },
+            {
+              "type": "text",
+              "attributes": { "subtype": "bulleted-list" },
+              "delta": [
+                {
+                  "insert": "Twitter",
+                  "attributes": { "href": "https://twitter.com/appflowy" }
+                },
+                { "insert": ": @appflowy" }
+              ]
+            },
+            {
+              "type": "text",
+              "attributes": { "subtype": "bulleted-list" },
+              "delta": [
+                {
+                  "insert": "Newsletter",
+                  "attributes": { "href": "https://blog-appflowy.ghost.io/" }
+                }
+              ]
+            }
+          ],
+          "attributes": { "emoji": "😀" }
+        },
+        { "type": "text", "delta": [] },
+        {
+          "type": "text",
+          "attributes": { "subtype": null, "heading": null },
+          "delta": []
+        },
+        {
+          "type": "text",
+          "attributes": { "subtype": null, "heading": null },
+          "delta": []
+        }
+      ]
+    }
+  }

+ 9 - 3
frontend/appflowy_flutter/lib/plugins/document/application/doc_bloc.dart

@@ -1,5 +1,6 @@
 import 'package:appflowy/plugins/document/application/document_data_pb_extension.dart';
 import 'package:appflowy/plugins/document/application/editor_transaction_adapter.dart';
+import 'package:appflowy/plugins/document/presentation/editor_plugins/migration/editor_migration.dart';
 import 'package:appflowy/plugins/trash/application/trash_service.dart';
 import 'package:appflowy/user/application/user_service.dart';
 import 'package:appflowy/util/json_print.dart';
@@ -14,6 +15,7 @@ import 'package:appflowy_editor/appflowy_editor.dart'
 import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
 import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
 import 'package:flutter/foundation.dart';
+import 'package:flutter/services.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
 import 'package:freezed_annotation/freezed_annotation.dart';
 import 'package:dartz/dartz.dart';
@@ -129,7 +131,7 @@ class DocumentBloc extends Bloc<DocumentEvent, DocumentState> {
     );
   }
 
-  Future<void> _initAppFlowyEditorState(DocumentDataPB2 data) async {
+  Future<void> _initAppFlowyEditorState(DocumentDataPB data) async {
     if (kDebugMode) {
       prettyPrintJson(data.toProto3Json());
     }
@@ -140,7 +142,11 @@ class DocumentBloc extends Bloc<DocumentEvent, DocumentState> {
       return;
     }
 
-    final editorState = EditorState(document: document);
+    // test: read from asset
+    final readme = await rootBundle.loadString('assets/template/readme.json');
+    final readmeDocument = EditorMigration.migrateDocument(readme);
+
+    final editorState = EditorState(document: readmeDocument);
     this.editorState = editorState;
 
     // subscribe to the document change from the editor
@@ -189,6 +195,6 @@ class DocumentState with _$DocumentState {
 class DocumentLoadingState with _$DocumentLoadingState {
   const factory DocumentLoadingState.loading() = _Loading;
   const factory DocumentLoadingState.finish(
-    Either<FlowyError, DocumentDataPB2> successOrFail,
+    Either<FlowyError, DocumentDataPB> successOrFail,
   ) = _Finish;
 }

+ 9 - 9
frontend/appflowy_flutter/lib/plugins/document/application/doc_service.dart

@@ -14,27 +14,27 @@ class DocumentService {
     if (canOpen.isRight()) {
       return const Right(unit);
     }
-    final payload = CreateDocumentPayloadPBV2()..documentId = view.id;
-    final result = await DocumentEvent2CreateDocument(payload).send();
+    final payload = CreateDocumentPayloadPB()..documentId = view.id;
+    final result = await DocumentEventCreateDocument(payload).send();
     return result.swap();
   }
 
-  Future<Either<FlowyError, DocumentDataPB2>> openDocument({
+  Future<Either<FlowyError, DocumentDataPB>> openDocument({
     required ViewPB view,
   }) async {
     // set the latest view
     await FolderEventSetLatestView(ViewIdPB(value: view.id)).send();
 
-    final payload = OpenDocumentPayloadPBV2()..documentId = view.id;
-    final result = await DocumentEvent2OpenDocument(payload).send();
+    final payload = OpenDocumentPayloadPB()..documentId = view.id;
+    final result = await DocumentEventOpenDocument(payload).send();
     return result.swap();
   }
 
   Future<Either<FlowyError, Unit>> closeDocument({
     required ViewPB view,
   }) async {
-    final payload = CloseDocumentPayloadPBV2()..documentId = view.id;
-    final result = await DocumentEvent2CloseDocument(payload).send();
+    final payload = CloseDocumentPayloadPB()..documentId = view.id;
+    final result = await DocumentEventCloseDocument(payload).send();
     return result.swap();
   }
 
@@ -42,11 +42,11 @@ class DocumentService {
     required String documentId,
     required Iterable<BlockActionPB> actions,
   }) async {
-    final payload = ApplyActionPayloadPBV2(
+    final payload = ApplyActionPayloadPB(
       documentId: documentId,
       actions: actions,
     );
-    final result = await DocumentEvent2ApplyAction(payload).send();
+    final result = await DocumentEventApplyAction(payload).send();
     return result.swap();
   }
 }

+ 19 - 2
frontend/appflowy_flutter/lib/plugins/document/application/document_data_pb_extension.dart

@@ -6,7 +6,19 @@ import 'package:appflowy_editor/appflowy_editor.dart'
     show Document, Node, Attributes, Delta, ParagraphBlockKeys;
 import 'package:collection/collection.dart';
 
-extension AppFlowyEditor on DocumentDataPB2 {
+extension AppFlowyEditor on DocumentDataPB {
+  DocumentDataPB? fromDocument(Document document) {
+    final blocks = <String, BlockPB>{};
+    final pageId = document.root.id;
+    final childrenMap = <String, ChildrenPB>{};
+    final meta = MetaPB(childrenMap: childrenMap);
+    return DocumentDataPB(
+      blocks: blocks,
+      pageId: pageId,
+      meta: meta,
+    );
+  }
+
   Document? toDocument() {
     final rootId = pageId;
     try {
@@ -78,12 +90,17 @@ extension BlockToNode on BlockPB {
 }
 
 extension NodeToBlock on Node {
-  BlockPB toBlock() {
+  BlockPB toBlock({
+    String? childrenId,
+  }) {
     assert(id.isNotEmpty);
     final block = BlockPB.create()
       ..id = id
       ..ty = _typeAdapter(type)
       ..data = _dataAdapter(type, attributes);
+    if (childrenId != null && childrenId.isNotEmpty) {
+      block.childrenId = childrenId;
+    }
     return block;
   }
 

+ 11 - 3
frontend/appflowy_flutter/lib/plugins/document/application/editor_transaction_adapter.dart

@@ -15,6 +15,8 @@ import 'package:appflowy_editor/appflowy_editor.dart'
 import 'package:collection/collection.dart';
 import 'dart:async';
 
+import 'package:nanoid/nanoid.dart';
+
 /// Uses to adjust the data structure between the editor and the backend.
 ///
 /// The editor uses a tree structure to represent the document, while the backend uses a flat structure.
@@ -64,14 +66,20 @@ extension on InsertOperation {
     for (final node in nodes) {
       final parentId =
           node.parent?.id ?? editorState.getNodeAtPath(path.parent)?.id ?? '';
-      final prevId = previousNode?.id ??
+      var prevId = previousNode?.id ??
           editorState.getNodeAtPath(path.previous)?.id ??
           '';
-      assert(parentId.isNotEmpty && prevId.isNotEmpty);
+      assert(parentId.isNotEmpty);
+      if (path.equals(path.previous)) {
+        prevId = '';
+      } else {
+        assert(prevId.isNotEmpty && prevId != node.id);
+      }
       final payload = BlockActionPayloadPB()
-        ..block = node.toBlock()
+        ..block = node.toBlock(childrenId: nanoid(10))
         ..parentId = parentId
         ..prevId = prevId;
+      assert(payload.block.childrenId.isNotEmpty);
       actions.add(
         BlockActionPB()
           ..action = BlockActionTypePB.Insert

+ 3 - 2
frontend/appflowy_flutter/lib/plugins/document/document_page.dart

@@ -9,7 +9,8 @@ import 'package:appflowy/plugins/document/presentation/export_page_widget.dart';
 import 'package:appflowy/startup/startup.dart';
 import 'package:appflowy/util/base64_string.dart';
 import 'package:appflowy/util/file_picker/file_picker_service.dart';
-import 'package:appflowy_backend/protobuf/flowy-document2/protobuf.dart';
+import 'package:appflowy_backend/protobuf/flowy-document2/protobuf.dart'
+    hide DocumentEvent;
 import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
 import 'package:appflowy_editor/appflowy_editor.dart';
 import 'package:flowy_infra_ui/flowy_infra_ui.dart';
@@ -119,7 +120,7 @@ class _DocumentPageState extends State<DocumentPage> {
     );
   }
 
-  Future<void> _exportPage(DocumentDataPB2 data) async {
+  Future<void> _exportPage(DocumentDataPB data) async {
     final picker = getIt<FilePickerService>();
     final dir = await picker.getDirectoryPath();
     if (dir == null) {

+ 1 - 0
frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart

@@ -225,6 +225,7 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
         if (supportColorBuilderTypes.contains(entry.key)) ...colorAction,
       ];
 
+      builder.showActions = (_) => true;
       builder.actionBuilder = (context, state) => BlockActionList(
             blockComponentContext: context,
             blockComponentState: state,

+ 2 - 0
frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/emoji_picker_button.dart

@@ -31,10 +31,12 @@ class EmojiPickerButton extends StatelessWidget {
       popupBuilder: (context) => _buildEmojiPicker(),
       child: FlowyTextButton(
         emoji,
+        overflow: TextOverflow.visible,
         fontSize: emojiSize,
         padding: EdgeInsets.zero,
         constraints: const BoxConstraints(minWidth: 35.0),
         fillColor: Colors.transparent,
+        mainAxisAlignment: MainAxisAlignment.center,
         onPressed: () {
           popoverController.show();
         },

+ 6 - 5
frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/callout/callout_block_component.dart

@@ -1,4 +1,5 @@
 import 'package:appflowy_editor/appflowy_editor.dart';
+import 'package:flowy_infra_ui/flowy_infra_ui.dart';
 import 'package:flutter/material.dart';
 import 'package:provider/provider.dart';
 import '../base/emoji_picker_button.dart';
@@ -147,7 +148,9 @@ class _CalloutBlockComponentWidgetState
           Padding(
             padding: const EdgeInsets.all(2.0),
             child: EmojiPickerButton(
-              key: ValueKey(emoji), // force to refresh the popover state
+              key: ValueKey(
+                emoji.toString(),
+              ), // force to refresh the popover state
               emoji: emoji,
               onSubmitted: (emoji, controller) {
                 setEmoji(emoji.emoji);
@@ -157,13 +160,11 @@ class _CalloutBlockComponentWidgetState
           ),
           Expanded(
             child: Padding(
-              padding: const EdgeInsets.symmetric(vertical: 6.0),
+              padding: const EdgeInsets.symmetric(vertical: 8.0),
               child: buildCalloutBlockComponent(context),
             ),
           ),
-          const SizedBox(
-            width: 10.0,
-          )
+          const VSpace(10),
         ],
       ),
     );

+ 4 - 4
frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/cover/change_cover_popover.dart

@@ -150,7 +150,7 @@ class _ChangeCoverPopoverState extends State<ChangeCoverPopover> {
               fontColor: Theme.of(context).colorScheme.tertiary,
               onPressed: () async {
                 final hasFileImageCover = CoverSelectionType.fromString(
-                      widget.node.attributes[kCoverSelectionTypeAttribute],
+                      widget.node.attributes[CoverBlockKeys.selectionType],
                     ) ==
                     CoverSelectionType.file;
                 final changeCoverBloc = context.read<ChangeCoverPopoverBloc>();
@@ -220,9 +220,9 @@ class _ChangeCoverPopoverState extends State<ChangeCoverPopover> {
       pickerBackgroundColor: theme.cardColor,
       pickerItemHoverColor: theme.hoverColor,
       selectedBackgroundColorHex:
-          widget.node.attributes[kCoverSelectionTypeAttribute] ==
+          widget.node.attributes[CoverBlockKeys.selectionType] ==
                   CoverSelectionType.color.toString()
-              ? widget.node.attributes[kCoverSelectionAttribute]
+              ? widget.node.attributes[CoverBlockKeys.selection]
               : 'ffffff',
       backgroundColorOptions:
           _generateBackgroundColorOptions(widget.editorState),
@@ -284,7 +284,7 @@ class _ChangeCoverPopoverState extends State<ChangeCoverPopover> {
                   final changeCoverBloc =
                       context.read<ChangeCoverPopoverBloc>();
                   final deletingCurrentCover =
-                      widget.node.attributes[kCoverSelectionAttribute] ==
+                      widget.node.attributes[CoverBlockKeys.selection] ==
                           images[index - 1];
                   if (deletingCurrentCover) {
                     await showDialog(

+ 5 - 4
frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/cover/change_cover_popover_bloc.dart

@@ -32,7 +32,7 @@ class ChangeCoverPopoverBloc
         deleteImage: (DeleteImage deleteImage) async {
           final currentState = state;
           final currentlySelectedImage =
-              node.attributes[kCoverSelectionAttribute];
+              node.attributes[CoverBlockKeys.selection];
           if (currentState is Loaded) {
             await _deleteImageInStorage(deleteImage.path);
             if (currentlySelectedImage == deleteImage.path) {
@@ -48,7 +48,7 @@ class ChangeCoverPopoverBloc
         clearAllImages: (ClearAllImages clearAllImages) async {
           final currentState = state;
           final currentlySelectedImage =
-              node.attributes[kCoverSelectionAttribute];
+              node.attributes[CoverBlockKeys.selection];
 
           if (currentState is Loaded) {
             for (final image in currentState.imageNames) {
@@ -90,8 +90,9 @@ class ChangeCoverPopoverBloc
   Future<void> _removeCoverImageFromNode() async {
     final transaction = editorState.transaction;
     transaction.updateNode(node, {
-      kCoverSelectionTypeAttribute: CoverSelectionType.initial.toString(),
-      kIconSelectionAttribute: node.attributes[kIconSelectionAttribute]
+      CoverBlockKeys.selectionType: CoverSelectionType.initial.toString(),
+      CoverBlockKeys.iconSelection:
+          node.attributes[CoverBlockKeys.iconSelection]
     });
     return editorState.apply(transaction);
   }

+ 46 - 40
frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/cover/cover_node_widget.dart

@@ -15,10 +15,13 @@ import 'package:flowy_infra_ui/flowy_infra_ui.dart';
 import 'package:flowy_infra_ui/widget/rounded_button.dart';
 import 'package:flutter/material.dart';
 
-const String kCoverType = 'cover';
-const String kCoverSelectionTypeAttribute = 'cover_selection_type';
-const String kCoverSelectionAttribute = 'cover_selection';
-const String kIconSelectionAttribute = 'selected_icon';
+class CoverBlockKeys {
+  const CoverBlockKeys._();
+
+  static const String selectionType = 'cover_selection_type';
+  static const String selection = 'cover_selection';
+  static const String iconSelection = 'selected_icon';
+}
 
 enum CoverSelectionType {
   initial,
@@ -69,7 +72,7 @@ class CoverImageNodeWidget extends StatefulWidget {
 
 class _CoverImageNodeWidgetState extends State<CoverImageNodeWidget> {
   CoverSelectionType get selectionType => CoverSelectionType.fromString(
-        widget.node.attributes[kCoverSelectionTypeAttribute],
+        widget.node.attributes[CoverBlockKeys.selectionType],
       );
 
   @override
@@ -105,9 +108,10 @@ class _CoverImageNodeWidgetState extends State<CoverImageNodeWidget> {
   Future<void> _insertCover(CoverSelectionType type, dynamic cover) async {
     final transaction = widget.editorState.transaction;
     transaction.updateNode(widget.node, {
-      kCoverSelectionTypeAttribute: type.toString(),
-      kCoverSelectionAttribute: cover,
-      kIconSelectionAttribute: widget.node.attributes[kIconSelectionAttribute]
+      CoverBlockKeys.selectionType: type.toString(),
+      CoverBlockKeys.selection: cover,
+      CoverBlockKeys.iconSelection:
+          widget.node.attributes[CoverBlockKeys.iconSelection]
     });
     return widget.editorState.apply(transaction);
   }
@@ -247,11 +251,11 @@ class _AddCoverButtonState extends State<_AddCoverButton> {
   Future<void> _insertIcon(Emoji emoji) async {
     final transaction = widget.editorState.transaction;
     transaction.updateNode(widget.node, {
-      kCoverSelectionTypeAttribute:
-          widget.node.attributes[kCoverSelectionTypeAttribute],
-      kCoverSelectionAttribute:
-          widget.node.attributes[kCoverSelectionAttribute],
-      kIconSelectionAttribute: emoji.emoji,
+      CoverBlockKeys.selectionType:
+          widget.node.attributes[CoverBlockKeys.selectionType],
+      CoverBlockKeys.selection:
+          widget.node.attributes[CoverBlockKeys.selection],
+      CoverBlockKeys.iconSelection: emoji.emoji,
     });
     return widget.editorState.apply(transaction);
   }
@@ -259,11 +263,11 @@ class _AddCoverButtonState extends State<_AddCoverButton> {
   Future<void> _removeIcon() async {
     final transaction = widget.editorState.transaction;
     transaction.updateNode(widget.node, {
-      kIconSelectionAttribute: "",
-      kCoverSelectionTypeAttribute:
-          widget.node.attributes[kCoverSelectionTypeAttribute],
-      kCoverSelectionAttribute:
-          widget.node.attributes[kCoverSelectionAttribute],
+      CoverBlockKeys.iconSelection: "",
+      CoverBlockKeys.selectionType:
+          widget.node.attributes[CoverBlockKeys.selectionType],
+      CoverBlockKeys.selection:
+          widget.node.attributes[CoverBlockKeys.selection],
     });
     return widget.editorState.apply(transaction);
   }
@@ -297,15 +301,16 @@ class _CoverImageState extends State<_CoverImage> {
   final popoverController = PopoverController();
 
   CoverSelectionType get selectionType => CoverSelectionType.fromString(
-        widget.node.attributes[kCoverSelectionTypeAttribute],
+        widget.node.attributes[CoverBlockKeys.selectionType],
       );
   Color get color => Color(
-        int.tryParse(widget.node.attributes[kCoverSelectionAttribute]) ??
+        int.tryParse(widget.node.attributes[CoverBlockKeys.selection]) ??
             0xFFFFFFFF,
       );
-  bool get hasIcon => widget.node.attributes[kIconSelectionAttribute] == null
-      ? false
-      : widget.node.attributes[kIconSelectionAttribute].isNotEmpty;
+  bool get hasIcon =>
+      widget.node.attributes[CoverBlockKeys.iconSelection] == null
+          ? false
+          : widget.node.attributes[CoverBlockKeys.iconSelection].isNotEmpty;
   bool isOverlayButtonsHidden = true;
   PopoverController iconPopoverController = PopoverController();
   bool get hasCover =>
@@ -336,7 +341,7 @@ class _CoverImageState extends State<_CoverImage> {
                   constraints: BoxConstraints.loose(const Size(320, 380)),
                   margin: EdgeInsets.zero,
                   child: EmojiIconWidget(
-                    emoji: widget.node.attributes[kIconSelectionAttribute],
+                    emoji: widget.node.attributes[CoverBlockKeys.iconSelection],
                   ),
                   popupBuilder: (BuildContext popoverContext) {
                     return EmojiPopover(
@@ -375,9 +380,10 @@ class _CoverImageState extends State<_CoverImage> {
   Future<void> _insertCover(CoverSelectionType type, dynamic cover) async {
     final transaction = widget.editorState.transaction;
     transaction.updateNode(widget.node, {
-      kCoverSelectionTypeAttribute: type.toString(),
-      kCoverSelectionAttribute: cover,
-      kIconSelectionAttribute: widget.node.attributes[kIconSelectionAttribute]
+      CoverBlockKeys.selectionType: type.toString(),
+      CoverBlockKeys.selection: cover,
+      CoverBlockKeys.iconSelection:
+          widget.node.attributes[CoverBlockKeys.iconSelection]
     });
     return widget.editorState.apply(transaction);
   }
@@ -385,11 +391,11 @@ class _CoverImageState extends State<_CoverImage> {
   Future<void> _insertIcon(Emoji emoji) async {
     final transaction = widget.editorState.transaction;
     transaction.updateNode(widget.node, {
-      kCoverSelectionTypeAttribute:
-          widget.node.attributes[kCoverSelectionTypeAttribute],
-      kCoverSelectionAttribute:
-          widget.node.attributes[kCoverSelectionAttribute],
-      kIconSelectionAttribute: emoji.emoji,
+      CoverBlockKeys.selectionType:
+          widget.node.attributes[CoverBlockKeys.selectionType],
+      CoverBlockKeys.selection:
+          widget.node.attributes[CoverBlockKeys.selection],
+      CoverBlockKeys.iconSelection: emoji.emoji,
     });
     return widget.editorState.apply(transaction);
   }
@@ -397,11 +403,11 @@ class _CoverImageState extends State<_CoverImage> {
   Future<void> _removeIcon() async {
     final transaction = widget.editorState.transaction;
     transaction.updateNode(widget.node, {
-      kIconSelectionAttribute: "",
-      kCoverSelectionTypeAttribute:
-          widget.node.attributes[kCoverSelectionTypeAttribute],
-      kCoverSelectionAttribute:
-          widget.node.attributes[kCoverSelectionAttribute],
+      CoverBlockKeys.iconSelection: "",
+      CoverBlockKeys.selectionType:
+          widget.node.attributes[CoverBlockKeys.selectionType],
+      CoverBlockKeys.selection:
+          widget.node.attributes[CoverBlockKeys.selection],
     });
     return widget.editorState.apply(transaction);
   }
@@ -409,7 +415,7 @@ class _CoverImageState extends State<_CoverImage> {
   Widget _buildCoverOverlayButtons(BuildContext context) {
     return Positioned(
       bottom: 20,
-      right: 260,
+      right: EditorStyleCustomizer.horizontalPadding,
       child: Row(
         mainAxisSize: MainAxisSize.min,
         children: [
@@ -480,7 +486,7 @@ class _CoverImageState extends State<_CoverImage> {
     switch (selectionType) {
       case CoverSelectionType.file:
         final imageFile =
-            File(widget.node.attributes[kCoverSelectionAttribute]);
+            File(widget.node.attributes[CoverBlockKeys.selection]);
         if (!imageFile.existsSync()) {
           // reset cover state
           WidgetsBinding.instance.addPostFrameCallback((_) {
@@ -496,7 +502,7 @@ class _CoverImageState extends State<_CoverImage> {
         break;
       case CoverSelectionType.asset:
         coverImage = Image.asset(
-          widget.node.attributes[kCoverSelectionAttribute],
+          widget.node.attributes[CoverBlockKeys.selection],
           fit: BoxFit.cover,
         );
         break;

+ 154 - 0
frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/migration/editor_migration.dart

@@ -0,0 +1,154 @@
+import 'dart:convert';
+
+import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
+import 'package:appflowy_editor/appflowy_editor.dart';
+import 'package:collection/collection.dart';
+
+class EditorMigration {
+  // AppFlowy 0.1.x -> 0.2
+  //
+  // The cover node has been deprecated, and use page/attributes/cover instead.
+  // cover node -> page/attributes/cover
+  //
+  // mark the textNode deprecated. use paragraph node instead.
+  // text node -> paragraph node
+  // delta -> attributes/delta
+  //
+  // mark the subtype deprecated. use type instead.
+  // for example, text/checkbox -> checkbox_list
+  //
+  // some attribute keys.
+  // ...
+  static Document migrateDocument(String json) {
+    final map = jsonDecode(json);
+    assert(map['document'] != null);
+    final documentV0 = Map<String, Object>.from(map['document'] as Map);
+    final rootV0 = NodeV0.fromJson(documentV0);
+    final root = migrateNode(rootV0);
+    return Document(root: root);
+  }
+
+  static Node migrateNode(NodeV0 nodeV0) {
+    Node? node;
+    final children = nodeV0.children.map((e) => migrateNode(e)).toList();
+    final id = nodeV0.id;
+    if (id == 'editor') {
+      final coverNode = children.firstWhereOrNull(
+        (element) => element.id == 'cover',
+      );
+      if (coverNode != null) {
+        node = documentNode(
+          children: children,
+          attributes: coverNode.attributes,
+        );
+      } else {
+        node = documentNode(children: children);
+      }
+    } else if (id == 'callout') {
+      final emoji = nodeV0.attributes['emoji'] ?? '📌';
+      final delta =
+          nodeV0.children.whereType<TextNodeV0>().fold(Delta(), (p, e) {
+        final delta = migrateDelta(e.delta);
+        final textInserts = delta.whereType<TextInsert>();
+        for (final element in textInserts) {
+          p.add(element);
+        }
+        return p..insert('\n');
+      });
+      node = calloutNode(
+        emoji: emoji,
+        delta: delta,
+      );
+    } else if (id == 'divider') {
+      // divider -> divider
+      node = dividerNode();
+    } else if (id == 'math_equation') {
+      // math_equation -> math_equation
+      final formula = nodeV0.attributes['math_equation'] ?? '';
+      node = mathEquationNode(formula: formula);
+    } else if (nodeV0 is TextNodeV0) {
+      final delta = migrateDelta(nodeV0.delta);
+      final deltaJson = delta.toJson();
+      final attributes = {'delta': deltaJson};
+      if (id == 'text') {
+        // text -> paragraph
+        node = paragraphNode(
+          attributes: attributes,
+          children: children,
+        );
+      } else if (nodeV0.id == 'text/heading') {
+        // text/heading -> heading
+        final heading = nodeV0.attributes.heading?.replaceAll('h', '');
+        final level = int.tryParse(heading ?? '') ?? 1;
+        node = headingNode(
+          level: level,
+          attributes: attributes,
+        );
+      } else if (id == 'text/checkbox') {
+        // text/checkbox -> todo_list
+        final checked = nodeV0.attributes.check;
+        node = todoListNode(
+          checked: checked,
+          attributes: attributes,
+          children: children,
+        );
+      } else if (id == 'text/quote') {
+        // text/quote -> quote
+        node = quoteNode(attributes: attributes);
+      } else if (id == 'text/number-list') {
+        // text/number-list -> numbered_list
+        node = numberedListNode(
+          attributes: attributes,
+          children: children,
+        );
+      } else if (id == 'text/bulleted-list') {
+        // text/bulleted-list -> bulleted_list
+        node = bulletedListNode(
+          attributes: attributes,
+          children: children,
+        );
+      } else if (id == 'text/code_block') {
+        // text/code_block -> code
+        final language = nodeV0.attributes['language'];
+        node = codeBlockNode(delta: delta, language: language);
+      }
+    } else if (id == 'cover') {
+      node = paragraphNode();
+    }
+
+    return node ?? paragraphNode(text: jsonEncode(nodeV0.toJson()));
+  }
+
+  // migrate the attributes.
+  // backgroundColor -> highlightColor
+  // color -> textColor
+  static Delta migrateDelta(Delta delta) {
+    final textInserts = delta
+        .whereType<TextInsert>()
+        .map(
+          (e) => TextInsert(
+            e.text,
+            attributes: migrateAttributes(e.attributes),
+          ),
+        )
+        .toList(growable: false);
+    return Delta(operations: textInserts.toList());
+  }
+
+  static Attributes? migrateAttributes(Attributes? attributes) {
+    if (attributes == null) {
+      return null;
+    }
+    const backgroundColor = 'backgroundColor';
+    if (attributes.containsKey(backgroundColor)) {
+      attributes['highlightColor'] = attributes[backgroundColor];
+      attributes.remove(backgroundColor);
+    }
+    const color = 'color';
+    if (attributes.containsKey(color)) {
+      attributes['textColor'] = attributes[color];
+      attributes.remove(color);
+    }
+    return attributes;
+  }
+}

+ 130 - 4
frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/openai/widgets/auto_completion_node_widget.dart

@@ -24,6 +24,7 @@ class AutoCompletionBlockKeys {
   static const String type = 'auto_completion';
   static const String prompt = 'prompt';
   static const String startSelection = 'start_selection';
+  static const String generationCount = 'generation_count';
 }
 
 Node autoCompletionNode({
@@ -35,6 +36,7 @@ Node autoCompletionNode({
     attributes: {
       AutoCompletionBlockKeys.prompt: prompt,
       AutoCompletionBlockKeys.startSelection: start.toJson(),
+      AutoCompletionBlockKeys.generationCount: 0,
     },
   );
 }
@@ -92,6 +94,16 @@ class _AutoCompletionBlockComponentState
   late final SelectionGestureInterceptor interceptor;
 
   String get prompt => widget.node.attributes[AutoCompletionBlockKeys.prompt];
+  int get generationCount =>
+      widget.node.attributes[AutoCompletionBlockKeys.generationCount] ?? 0;
+  Selection? get startSelection {
+    final selection =
+        widget.node.attributes[AutoCompletionBlockKeys.startSelection];
+    if (selection != null) {
+      return Selection.fromJson(selection);
+    }
+    return null;
+  }
 
   @override
   void initState() {
@@ -106,6 +118,7 @@ class _AutoCompletionBlockComponentState
 
   @override
   void dispose() {
+    _onExit();
     _unsubscribeSelectionGesture();
     controller.dispose();
 
@@ -124,7 +137,7 @@ class _AutoCompletionBlockComponentState
           children: [
             const AutoCompletionHeader(),
             const Space(0, 10),
-            if (prompt.isEmpty) ...[
+            if (prompt.isEmpty && generationCount < 1) ...[
               _buildInputWidget(context),
               const Space(0, 10),
               AutoCompletionInputFooter(
@@ -134,6 +147,7 @@ class _AutoCompletionBlockComponentState
             ] else ...[
               AutoCompletionFooter(
                 onKeep: _onExit,
+                onRewrite: _onRewrite,
                 onDiscard: _onDiscard,
               )
             ],
@@ -213,13 +227,13 @@ class _AutoCompletionBlockComponentState
         await _showError(error.message);
       },
     );
+    await _updateGenerationCount();
   }
 
   Future<void> _onDiscard() async {
-    final selection =
-        widget.node.attributes[AutoCompletionBlockKeys.startSelection];
+    final selection = startSelection;
     if (selection != null) {
-      final start = Selection.fromJson(selection).start.path;
+      final start = selection.start.path;
       final end = widget.node.previous?.path;
       if (end != null) {
         final transaction = editorState.transaction;
@@ -228,11 +242,101 @@ class _AutoCompletionBlockComponentState
           end.last - start.last + 1,
         );
         await editorState.apply(transaction);
+        await _makeSurePreviousNodeIsEmptyParagraphNode();
       }
     }
     _onExit();
   }
 
+  Future<void> _onRewrite() async {
+    final previousOutput = _getPreviousOutput();
+    if (previousOutput == null) {
+      return;
+    }
+
+    final loading = Loading(context);
+    loading.start();
+    // clear previous response
+    final selection = startSelection;
+    if (selection != null) {
+      final start = selection.start.path;
+      final end = widget.node.previous?.path;
+      if (end != null) {
+        final transaction = editorState.transaction;
+        transaction.deleteNodesAtPath(
+          start,
+          end.last - start.last + 1,
+        );
+        await editorState.apply(transaction);
+      }
+    }
+    // generate new response
+    final userProfile = await UserBackendService.getCurrentUserProfile()
+        .then((value) => value.toOption().toNullable());
+    if (userProfile == null) {
+      loading.stop();
+      await _showError(
+        LocaleKeys.document_plugins_autoGeneratorCantGetOpenAIKey.tr(),
+      );
+      return;
+    }
+    final textRobot = TextRobot(editorState: editorState);
+    final openAIRepository = HttpOpenAIRepository(
+      client: http.Client(),
+      apiKey: userProfile.openaiKey,
+    );
+    await openAIRepository.getStreamedCompletions(
+      prompt: _rewritePrompt(previousOutput),
+      onStart: () async {
+        loading.stop();
+        await _makeSurePreviousNodeIsEmptyParagraphNode();
+      },
+      onProcess: (response) async {
+        if (response.choices.isNotEmpty) {
+          final text = response.choices.first.text;
+          await textRobot.autoInsertText(
+            text,
+            inputType: TextRobotInputType.word,
+            delay: Duration.zero,
+          );
+        }
+      },
+      onEnd: () async {},
+      onError: (error) async {
+        loading.stop();
+        await _showError(error.message);
+      },
+    );
+    await _updateGenerationCount();
+  }
+
+  String? _getPreviousOutput() {
+    final startSelection = this.startSelection;
+    if (startSelection != null) {
+      final end = widget.node.previous?.path;
+
+      if (end != null) {
+        final result = editorState
+            .getNodesInSelection(
+          startSelection.copyWith(end: Position(path: end)),
+        )
+            .fold(
+          '',
+          (previousValue, element) {
+            final delta = element.delta;
+            if (delta != null) {
+              return "$previousValue\n${delta.toPlainText()}";
+            } else {
+              return previousValue;
+            }
+          },
+        );
+        return result.trim();
+      }
+    }
+    return null;
+  }
+
   Future<void> _updateEditingText() async {
     final transaction = editorState.transaction;
     transaction.updateNode(
@@ -244,6 +348,21 @@ class _AutoCompletionBlockComponentState
     await editorState.apply(transaction);
   }
 
+  Future<void> _updateGenerationCount() async {
+    final transaction = editorState.transaction;
+    transaction.updateNode(
+      widget.node,
+      {
+        AutoCompletionBlockKeys.generationCount: generationCount + 1,
+      },
+    );
+    await editorState.apply(transaction);
+  }
+
+  String _rewritePrompt(String previousOutput) {
+    return 'I am not satisfied with your previous response ($previousOutput) to the query ($prompt). Please provide an alternative response.';
+  }
+
   Future<void> _makeSurePreviousNodeIsEmptyParagraphNode() async {
     // make sure the previous node is a empty paragraph node without any styles.
     final transaction = editorState.transaction;
@@ -393,10 +512,12 @@ class AutoCompletionFooter extends StatelessWidget {
   const AutoCompletionFooter({
     super.key,
     required this.onKeep,
+    required this.onRewrite,
     required this.onDiscard,
   });
 
   final VoidCallback onKeep;
+  final VoidCallback onRewrite;
   final VoidCallback onDiscard;
 
   @override
@@ -408,6 +529,11 @@ class AutoCompletionFooter extends StatelessWidget {
           onPressed: onKeep,
         ),
         const Space(10, 0),
+        SecondaryTextButton(
+          LocaleKeys.document_plugins_autoGeneratorRewrite.tr(),
+          onPressed: onRewrite,
+        ),
+        const Space(10, 0),
         SecondaryTextButton(
           LocaleKeys.button_discard.tr(),
           onPressed: onDiscard,

+ 0 - 4
frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/openai/widgets/smart_edit_node_widget.dart

@@ -15,10 +15,6 @@ import 'package:easy_localization/easy_localization.dart';
 import 'package:http/http.dart' as http;
 import 'package:provider/provider.dart';
 
-const String kSmartEditType = 'smart_edit_input';
-const String kSmartEditInstructionType = 'smart_edit_instruction';
-const String kSmartEditInputType = 'smart_edit_input';
-
 class SmartEditBlockKeys {
   const SmartEditBlockKeys._();
 

+ 4 - 7
frontend/appflowy_flutter/lib/plugins/document/presentation/editor_style.dart

@@ -11,7 +11,7 @@ class EditorStyleCustomizer {
   });
 
   static double get horizontalPadding =>
-      PlatformExtension.isDesktop ? 100.0 : 10.0;
+      PlatformExtension.isDesktop ? 50.0 : 10.0;
 
   final BuildContext context;
 
@@ -28,21 +28,18 @@ class EditorStyleCustomizer {
     final theme = Theme.of(context);
     final fontSize = context.read<DocumentAppearanceCubit>().state.fontSize;
     return EditorStyle.desktop(
-      padding: EdgeInsets.only(
-        left: horizontalPadding / 2.0,
-        right: horizontalPadding,
-      ),
+      padding: EdgeInsets.symmetric(horizontal: horizontalPadding),
       backgroundColor: theme.colorScheme.surface,
       cursorColor: theme.colorScheme.primary,
       textStyleConfiguration: TextStyleConfiguration(
         text: TextStyle(
-          fontFamily: 'poppins',
+          fontFamily: 'Poppins',
           fontSize: fontSize,
           color: theme.colorScheme.onBackground,
           height: 1.5,
         ),
         bold: const TextStyle(
-          fontFamily: 'poppins-Bold',
+          fontFamily: 'Poppins-Bold',
           fontWeight: FontWeight.w600,
         ),
         italic: const TextStyle(fontStyle: FontStyle.italic),

+ 11 - 14
frontend/appflowy_flutter/lib/plugins/document/presentation/export_page_widget.dart

@@ -1,3 +1,4 @@
+import 'package:flowy_infra_ui/widget/rounded_button.dart';
 import 'package:flutter/material.dart';
 import 'package:flowy_infra_ui/flowy_infra_ui.dart';
 
@@ -15,24 +16,20 @@ class ExportPageWidget extends StatelessWidget {
       mainAxisAlignment: MainAxisAlignment.center,
       mainAxisSize: MainAxisSize.min,
       children: [
-        const FlowyText.regular(
-          'There are some errors.',
-          fontSize: 16.0,
-        ),
-        const SizedBox(
-          height: 10,
+        const FlowyText.medium(
+          'Open document failed',
+          fontSize: 18.0,
         ),
+        const VSpace(5),
         const FlowyText.regular(
           'Please try to export the page and contact us.',
-          fontSize: 14.0,
-        ),
-        const SizedBox(
-          height: 5,
+          fontSize: 12.0,
         ),
-        FlowyTextButton(
-          'Export page',
-          constraints: const BoxConstraints(maxWidth: 100),
-          mainAxisAlignment: MainAxisAlignment.center,
+        const VSpace(20),
+        RoundedTextButton(
+          title: 'Export page',
+          width: 100,
+          height: 30,
           onPressed: onTap,
         )
       ],

+ 2 - 2
frontend/appflowy_flutter/pubspec.lock

@@ -53,8 +53,8 @@ packages:
     dependency: "direct main"
     description:
       path: "."
-      ref: b1a1b14
-      resolved-ref: b1a1b14f35114a7becdb3e2de909d546d7328a59
+      ref: ead61af
+      resolved-ref: ead61afb796037e8ceb63ba4bcf439818514ed4b
       url: "https://github.com/LucasXu0/appflowy-editor.git"
     source: git
     version: "0.1.12"

+ 2 - 1
frontend/appflowy_flutter/pubspec.yaml

@@ -47,7 +47,7 @@ dependencies:
     # path: /Users/lucas.xu/Desktop/appflowy-editor
     git:
       url: https://github.com/LucasXu0/appflowy-editor.git
-      ref: b1a1b14
+      ref: ead61af
   appflowy_popover:
     path: packages/appflowy_popover
 
@@ -179,6 +179,7 @@ flutter:
     - assets/images/login/
     - assets/images/grid/setting/
     - assets/translations/
+    - assets/template/readme.json
 
     # The following assets will be excluded in release.
     # BEGIN: EXCLUDE_IN_RELEASE

+ 18 - 18
frontend/appflowy_tauri/src/appflowy_app/stores/effects/document/document_bd_svc.ts

@@ -1,49 +1,49 @@
 import {
   FlowyError,
-  DocumentDataPB2,
-  OpenDocumentPayloadPBV2,
-  CreateDocumentPayloadPBV2,
-  ApplyActionPayloadPBV2,
+  DocumentDataPB,
+  OpenDocumentPayloadPB,
+  CreateDocumentPayloadPB,
+  ApplyActionPayloadPB,
   BlockActionPB,
-  CloseDocumentPayloadPBV2,
+  CloseDocumentPayloadPB,
 } from '@/services/backend';
 import { Result } from 'ts-results';
 import {
-  DocumentEvent2ApplyAction,
-  DocumentEvent2CloseDocument,
-  DocumentEvent2OpenDocument,
-  DocumentEvent2CreateDocument,
+  DocumentEventApplyAction,
+  DocumentEventCloseDocument,
+  DocumentEventOpenDocument,
+  DocumentEventCreateDocument,
 } from '@/services/backend/events/flowy-document2';
 
 export class DocumentBackendService {
   constructor(public readonly viewId: string) {}
 
   create = (): Promise<Result<void, FlowyError>> => {
-    const payload = CreateDocumentPayloadPBV2.fromObject({
+    const payload = CreateDocumentPayloadPB.fromObject({
       document_id: this.viewId,
     });
-    return DocumentEvent2CreateDocument(payload);
+    return DocumentEventCreateDocument(payload);
   };
 
-  open = (): Promise<Result<DocumentDataPB2, FlowyError>> => {
-    const payload = OpenDocumentPayloadPBV2.fromObject({
+  open = (): Promise<Result<DocumentDataPB, FlowyError>> => {
+    const payload = OpenDocumentPayloadPB.fromObject({
       document_id: this.viewId,
     });
-    return DocumentEvent2OpenDocument(payload);
+    return DocumentEventOpenDocument(payload);
   };
 
   applyActions = (actions: ReturnType<typeof BlockActionPB.prototype.toObject>[]): Promise<Result<void, FlowyError>> => {
-    const payload = ApplyActionPayloadPBV2.fromObject({
+    const payload = ApplyActionPayloadPB.fromObject({
       document_id: this.viewId,
       actions: actions,
     });
-    return DocumentEvent2ApplyAction(payload);
+    return DocumentEventApplyAction(payload);
   };
 
   close = (): Promise<Result<void, FlowyError>> => {
-    const payload = CloseDocumentPayloadPBV2.fromObject({
+    const payload = CloseDocumentPayloadPB.fromObject({
       document_id: this.viewId,
     });
-    return DocumentEvent2CloseDocument(payload);
+    return DocumentEventCloseDocument(payload);
   };
 }

+ 1 - 0
frontend/rust-lib/Cargo.lock

@@ -1647,6 +1647,7 @@ dependencies = [
 name = "flowy-document2"
 version = "0.1.0"
 dependencies = [
+ "anyhow",
  "appflowy-integrate",
  "bytes",
  "collab",

+ 1 - 0
frontend/rust-lib/flowy-document2/Cargo.toml

@@ -25,6 +25,7 @@ serde = { version = "1.0", features = ["derive"] }
 serde_json = {version = "1.0"}
 tracing = { version = "0.1", features = ["log"] }
 tokio = { version = "1.26", features = ["full"] }
+anyhow = "1.0"
 
 [dev-dependencies]
 tempfile = "3.4.0"

+ 27 - 2
frontend/rust-lib/flowy-document2/src/document_data.rs

@@ -3,12 +3,12 @@ use std::{collections::HashMap, vec};
 use collab_document::blocks::{Block, DocumentData, DocumentMeta};
 use nanoid::nanoid;
 
-use crate::entities::{BlockPB, ChildrenPB, DocumentDataPB2, MetaPB};
+use crate::entities::{BlockPB, ChildrenPB, DocumentDataPB, MetaPB};
 
 #[derive(Clone, Debug)]
 pub struct DocumentDataWrapper(pub DocumentData);
 
-impl From<DocumentDataWrapper> for DocumentDataPB2 {
+impl From<DocumentDataWrapper> for DocumentDataPB {
   fn from(data: DocumentDataWrapper) -> Self {
     let blocks = data
       .0
@@ -35,6 +35,31 @@ impl From<DocumentDataWrapper> for DocumentDataPB2 {
   }
 }
 
+impl From<DocumentDataPB> for DocumentDataWrapper {
+  fn from(data: DocumentDataPB) -> Self {
+    let blocks = data
+      .blocks
+      .into_iter()
+      .map(|(id, block)| (id, block.into()))
+      .collect();
+
+    let children_map = data
+      .meta
+      .children_map
+      .into_iter()
+      .map(|(id, children)| (id, children.children))
+      .collect();
+
+    let page_id = data.page_id;
+
+    Self(DocumentData {
+      page_id,
+      blocks,
+      meta: DocumentMeta { children_map },
+    })
+  }
+}
+
 // the default document data contains a page block and a text block
 impl Default for DocumentDataWrapper {
   fn default() -> Self {

+ 9 - 9
frontend/rust-lib/flowy-document2/src/entities.rs

@@ -3,28 +3,28 @@ use std::collections::HashMap;
 use flowy_derive::{ProtoBuf, ProtoBuf_Enum};
 
 #[derive(Default, ProtoBuf)]
-pub struct OpenDocumentPayloadPBV2 {
+pub struct OpenDocumentPayloadPB {
   #[pb(index = 1)]
   pub document_id: String,
-  // Support customize initial data
 }
 
 #[derive(Default, ProtoBuf)]
-pub struct CreateDocumentPayloadPBV2 {
+pub struct CreateDocumentPayloadPB {
   #[pb(index = 1)]
   pub document_id: String,
-  // Support customize initial data
+
+  #[pb(index = 2, one_of)]
+  pub initial_data: Option<DocumentDataPB>,
 }
 
 #[derive(Default, ProtoBuf)]
-pub struct CloseDocumentPayloadPBV2 {
+pub struct CloseDocumentPayloadPB {
   #[pb(index = 1)]
   pub document_id: String,
-  // Support customize initial data
 }
 
 #[derive(Default, ProtoBuf, Debug)]
-pub struct ApplyActionPayloadPBV2 {
+pub struct ApplyActionPayloadPB {
   #[pb(index = 1)]
   pub document_id: String,
 
@@ -33,14 +33,14 @@ pub struct ApplyActionPayloadPBV2 {
 }
 
 #[derive(Default, ProtoBuf)]
-pub struct GetDocumentDataPayloadPBV2 {
+pub struct GetDocumentDataPayloadPB {
   #[pb(index = 1)]
   pub document_id: String,
   // Support customize initial data
 }
 
 #[derive(Default, ProtoBuf)]
-pub struct DocumentDataPB2 {
+pub struct DocumentDataPB {
   #[pb(index = 1)]
   pub page_id: String,
 

+ 17 - 15
frontend/rust-lib/flowy-document2/src/event_handler.rs

@@ -17,38 +17,40 @@ use lib_dispatch::prelude::{data_result_ok, AFPluginData, AFPluginState, DataRes
 use crate::{
   document_data::DocumentDataWrapper,
   entities::{
-    ApplyActionPayloadPBV2, BlockActionPB, BlockActionPayloadPB, BlockActionTypePB, BlockEventPB,
-    BlockEventPayloadPB, BlockPB, CloseDocumentPayloadPBV2, CreateDocumentPayloadPBV2, DeltaTypePB,
-    DocEventPB, DocumentDataPB2, OpenDocumentPayloadPBV2,
+    ApplyActionPayloadPB, BlockActionPB, BlockActionPayloadPB, BlockActionTypePB, BlockEventPB,
+    BlockEventPayloadPB, BlockPB, CloseDocumentPayloadPB, CreateDocumentPayloadPB, DeltaTypePB,
+    DocEventPB, DocumentDataPB, OpenDocumentPayloadPB,
   },
   manager::DocumentManager,
 };
 
 // Handler for creating a new document
 pub(crate) async fn create_document_handler(
-  data: AFPluginData<CreateDocumentPayloadPBV2>,
+  data: AFPluginData<CreateDocumentPayloadPB>,
   manager: AFPluginState<Arc<DocumentManager>>,
 ) -> FlowyResult<()> {
   let context = data.into_inner();
-  // Create a new document with a default content, one page block and one text block
-  let data = DocumentDataWrapper::default();
-  manager.create_document(context.document_id, data)?;
+  let initial_data: DocumentDataWrapper = context
+    .initial_data
+    .map(|data| data.into())
+    .unwrap_or_default();
+  manager.create_document(context.document_id, initial_data)?;
   Ok(())
 }
 
 // Handler for opening an existing document
 pub(crate) async fn open_document_handler(
-  data: AFPluginData<OpenDocumentPayloadPBV2>,
+  data: AFPluginData<OpenDocumentPayloadPB>,
   manager: AFPluginState<Arc<DocumentManager>>,
-) -> DataResult<DocumentDataPB2, FlowyError> {
+) -> DataResult<DocumentDataPB, FlowyError> {
   let context = data.into_inner();
   let document = manager.open_document(context.document_id)?;
   let document_data = document.lock().get_document()?;
-  data_result_ok(DocumentDataPB2::from(DocumentDataWrapper(document_data)))
+  data_result_ok(DocumentDataPB::from(DocumentDataWrapper(document_data)))
 }
 
 pub(crate) async fn close_document_handler(
-  data: AFPluginData<CloseDocumentPayloadPBV2>,
+  data: AFPluginData<CloseDocumentPayloadPB>,
   manager: AFPluginState<Arc<DocumentManager>>,
 ) -> FlowyResult<()> {
   let context = data.into_inner();
@@ -59,18 +61,18 @@ pub(crate) async fn close_document_handler(
 // Get the content of the existing document,
 //  if the document does not exist, return an error.
 pub(crate) async fn get_document_data_handler(
-  data: AFPluginData<OpenDocumentPayloadPBV2>,
+  data: AFPluginData<OpenDocumentPayloadPB>,
   manager: AFPluginState<Arc<DocumentManager>>,
-) -> DataResult<DocumentDataPB2, FlowyError> {
+) -> DataResult<DocumentDataPB, FlowyError> {
   let context = data.into_inner();
   let document = manager.get_document(context.document_id)?;
   let document_data = document.lock().get_document()?;
-  data_result_ok(DocumentDataPB2::from(DocumentDataWrapper(document_data)))
+  data_result_ok(DocumentDataPB::from(DocumentDataWrapper(document_data)))
 }
 
 // Handler for applying an action to a document
 pub(crate) async fn apply_action_handler(
-  data: AFPluginData<ApplyActionPayloadPBV2>,
+  data: AFPluginData<ApplyActionPayloadPB>,
   manager: AFPluginState<Arc<DocumentManager>>,
 ) -> FlowyResult<()> {
   let context = data.into_inner();

+ 11 - 11
frontend/rust-lib/flowy-document2/src/event_map.rs

@@ -17,30 +17,30 @@ pub fn init(document_manager: Arc<DocumentManager>) -> AFPlugin {
     .name(env!("CARGO_PKG_NAME"))
     .state(document_manager);
 
-  plugin = plugin.event(DocumentEvent2::OpenDocument, open_document_handler);
-  plugin = plugin.event(DocumentEvent2::CloseDocument, close_document_handler);
-  plugin = plugin.event(DocumentEvent2::ApplyAction, apply_action_handler);
-  plugin = plugin.event(DocumentEvent2::CreateDocument, create_document_handler);
-  plugin = plugin.event(DocumentEvent2::GetDocumentData, get_document_data_handler);
+  plugin = plugin.event(DocumentEvent::CreateDocument, create_document_handler);
+  plugin = plugin.event(DocumentEvent::OpenDocument, open_document_handler);
+  plugin = plugin.event(DocumentEvent::CloseDocument, close_document_handler);
+  plugin = plugin.event(DocumentEvent::ApplyAction, apply_action_handler);
+  plugin = plugin.event(DocumentEvent::GetDocumentData, get_document_data_handler);
 
   plugin
 }
 
 #[derive(Debug, Clone, PartialEq, Eq, Hash, Display, ProtoBuf_Enum, Flowy_Event)]
 #[event_err = "FlowyError"]
-pub enum DocumentEvent2 {
-  #[event(input = "CreateDocumentPayloadPBV2")]
+pub enum DocumentEvent {
+  #[event(input = "CreateDocumentPayloadPB")]
   CreateDocument = 0,
 
-  #[event(input = "OpenDocumentPayloadPBV2", output = "DocumentDataPB2")]
+  #[event(input = "OpenDocumentPayloadPB", output = "DocumentDataPB")]
   OpenDocument = 1,
 
-  #[event(input = "CloseDocumentPayloadPBV2")]
+  #[event(input = "CloseDocumentPayloadPB")]
   CloseDocument = 2,
 
-  #[event(input = "ApplyActionPayloadPBV2")]
+  #[event(input = "ApplyActionPayloadPB")]
   ApplyAction = 3,
 
-  #[event(input = "GetDocumentDataPayloadPBV2")]
+  #[event(input = "GetDocumentDataPayloadPB")]
   GetDocumentData = 4,
 }