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
This commit is contained in:
Lucas.Xu 2023-05-23 16:13:12 +08:00 committed by GitHub
parent f8d09e4894
commit ffff628359
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 757 additions and 160 deletions

View File

@ -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": []
}
]
}
}

View File

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

View File

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

View File

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

View File

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

View File

@ -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) {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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,39 @@ 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;
transaction.deleteNodesAtPath(
start,
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;
@ -230,7 +270,71 @@ class _AutoCompletionBlockComponentState
await editorState.apply(transaction);
}
}
_onExit();
// 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 {
@ -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,

View File

@ -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._();

View File

@ -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),

View File

@ -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,
fontSize: 12.0,
),
const SizedBox(
height: 5,
),
FlowyTextButton(
'Export page',
constraints: const BoxConstraints(maxWidth: 100),
mainAxisAlignment: MainAxisAlignment.center,
const VSpace(20),
RoundedTextButton(
title: 'Export page',
width: 100,
height: 30,
onPressed: onTap,
)
],

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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,
}