From 2ef72f320371914aac733bf3733a2ebbb6f28c42 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Thu, 1 Jun 2023 20:18:40 +0800 Subject: [PATCH] feat: load the readme file when first time launch (#2676) * chore: bump version 0.2.0 * test: add migration test * feat: support exporting document data to afdoc format (#2610) * feat: support exporting document data to afdoc format * feat: export database * fix: resolve comment issues * fix: add error tips when exporting files failed --------- Co-authored-by: nathan * feat: rename delta * feat: add built-in readme * fix: use independent children id * fix: comment typo * chore: add icons * fix: floating toolbar style * fix: markdown export test * chore: disbale moveup/movedown action * fix: unit test --------- Co-authored-by: nathan --- frontend/Makefile.toml | 2 +- .../assets/images/editor/add.svg | 5 +- .../assets/images/editor/color_formatter.svg | 29 +++ .../assets/images/editor/delete.svg | 8 +- .../assets/images/editor/duplicate.svg | 4 +- .../assets/images/editor/option.svg | 9 +- .../assets/translations/en.json | 5 +- .../document_data_pb_extension.dart | 3 - .../document/presentation/editor_page.dart | 13 +- .../actions/block_action_button.dart | 2 +- .../actions/block_action_option_button.dart | 3 - .../editor_plugins/actions/option_action.dart | 20 +- .../actions/option_action_button.dart | 3 - .../migration/editor_migration.dart | 9 +- .../widgets/auto_completion_node_widget.dart | 2 +- .../document/presentation/editor_style.dart | 7 + .../settings_file_exporter_cubit.dart | 14 ++ .../widgets/settings_export_file_widget.dart | 73 ++++++ .../settings_file_exporter_widget.dart | 155 ++++++++++--- .../widgets/settings_file_system_view.dart | 21 +- frontend/appflowy_flutter/pubspec.lock | 11 +- frontend/appflowy_flutter/pubspec.yaml | 7 +- .../editor/editor_migration_test.dart | 15 +- .../unit_test/editor/share_markdown_test.dart | 10 +- frontend/rust-lib/Cargo.lock | 1 + .../rust-lib/flowy-core/assets/read_me.json | 219 ++++++++++++++++++ .../src/deps_resolve/folder2_deps.rs | 7 +- .../flowy-database/src/services/export.rs | 182 +++++++++++++++ frontend/rust-lib/flowy-document2/Cargo.toml | 1 + .../rust-lib/flowy-document2/src/entities.rs | 6 +- frontend/rust-lib/flowy-document2/src/lib.rs | 1 + .../flowy-document2/src/parser/json/block.rs | 20 ++ .../flowy-document2/src/parser/json/mod.rs | 2 + .../flowy-document2/src/parser/json/parser.rs | 81 +++++++ .../flowy-document2/src/parser/mod.rs | 1 + .../rust-lib/flowy-document2/tests/main.rs | 1 + .../tests/parser/json/block_test.rs | 102 ++++++++ .../flowy-document2/tests/parser/json/mod.rs | 2 + .../tests/parser/json/parser_test.rs | 103 ++++++++ .../flowy-document2/tests/parser/mod.rs | 1 + .../rust-lib/flowy-folder2/src/READ_ME.json | 181 --------------- 41 files changed, 1043 insertions(+), 298 deletions(-) create mode 100644 frontend/appflowy_flutter/assets/images/editor/color_formatter.svg create mode 100644 frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_export_file_widget.dart create mode 100644 frontend/rust-lib/flowy-core/assets/read_me.json create mode 100644 frontend/rust-lib/flowy-database/src/services/export.rs create mode 100644 frontend/rust-lib/flowy-document2/src/parser/json/block.rs create mode 100644 frontend/rust-lib/flowy-document2/src/parser/json/mod.rs create mode 100644 frontend/rust-lib/flowy-document2/src/parser/json/parser.rs create mode 100644 frontend/rust-lib/flowy-document2/src/parser/mod.rs create mode 100644 frontend/rust-lib/flowy-document2/tests/parser/json/block_test.rs create mode 100644 frontend/rust-lib/flowy-document2/tests/parser/json/mod.rs create mode 100644 frontend/rust-lib/flowy-document2/tests/parser/json/parser_test.rs create mode 100644 frontend/rust-lib/flowy-document2/tests/parser/mod.rs delete mode 100644 frontend/rust-lib/flowy-folder2/src/READ_ME.json diff --git a/frontend/Makefile.toml b/frontend/Makefile.toml index 2d79677721..b12a6fea7f 100644 --- a/frontend/Makefile.toml +++ b/frontend/Makefile.toml @@ -23,7 +23,7 @@ CARGO_MAKE_EXTEND_WORKSPACE_MAKEFILE = true CARGO_MAKE_CRATE_FS_NAME = "dart_ffi" CARGO_MAKE_CRATE_NAME = "dart-ffi" LIB_NAME = "dart_ffi" -CURRENT_APP_VERSION = "0.1.5" +CURRENT_APP_VERSION = "0.2.0" FLUTTER_DESKTOP_FEATURES = "dart,rev-sqlite" PRODUCT_NAME = "AppFlowy" # CRATE_TYPE: https://doc.rust-lang.org/reference/linkage.html diff --git a/frontend/appflowy_flutter/assets/images/editor/add.svg b/frontend/appflowy_flutter/assets/images/editor/add.svg index 697d992062..c1f9ce4e07 100644 --- a/frontend/appflowy_flutter/assets/images/editor/add.svg +++ b/frontend/appflowy_flutter/assets/images/editor/add.svg @@ -1 +1,4 @@ - \ No newline at end of file + + + + diff --git a/frontend/appflowy_flutter/assets/images/editor/color_formatter.svg b/frontend/appflowy_flutter/assets/images/editor/color_formatter.svg new file mode 100644 index 0000000000..35d6d2ed67 --- /dev/null +++ b/frontend/appflowy_flutter/assets/images/editor/color_formatter.svg @@ -0,0 +1,29 @@ + + + + + + + + + diff --git a/frontend/appflowy_flutter/assets/images/editor/delete.svg b/frontend/appflowy_flutter/assets/images/editor/delete.svg index fcfbf2f6dd..cdf24226b4 100644 --- a/frontend/appflowy_flutter/assets/images/editor/delete.svg +++ b/frontend/appflowy_flutter/assets/images/editor/delete.svg @@ -1,6 +1,6 @@ - - - - + + + + diff --git a/frontend/appflowy_flutter/assets/images/editor/duplicate.svg b/frontend/appflowy_flutter/assets/images/editor/duplicate.svg index 40b5ed5a95..1ae2067c24 100644 --- a/frontend/appflowy_flutter/assets/images/editor/duplicate.svg +++ b/frontend/appflowy_flutter/assets/images/editor/duplicate.svg @@ -1,4 +1,4 @@ - - + + diff --git a/frontend/appflowy_flutter/assets/images/editor/option.svg b/frontend/appflowy_flutter/assets/images/editor/option.svg index 25e2c334ea..627c959f9f 100644 --- a/frontend/appflowy_flutter/assets/images/editor/option.svg +++ b/frontend/appflowy_flutter/assets/images/editor/option.svg @@ -1 +1,8 @@ - \ No newline at end of file + + + + + + + + diff --git a/frontend/appflowy_flutter/assets/translations/en.json b/frontend/appflowy_flutter/assets/translations/en.json index 45d23aa9b8..5349359a63 100644 --- a/frontend/appflowy_flutter/assets/translations/en.json +++ b/frontend/appflowy_flutter/assets/translations/en.json @@ -187,6 +187,7 @@ "files": { "copy": "Copy", "defaultLocation": "Read files and data storage location", + "exportData": "Export your data", "doubleTapToCopy": "Double tap to copy the path", "restoreLocation": "Restore to AppFlowy default path", "customizeLocation": "Open another folder", @@ -209,7 +210,9 @@ "changeLocationTooltips": "Change the files read the data directory", "change": "Change", "openLocationTooltips": "Open the files read the data directory", - "recoverLocationTooltips": "Recover the files read the data directory" + "recoverLocationTooltips": "Recover the files read the data directory", + "exportFileSuccess": "Export file successfully!", + "exportFileFail": "Export file failed!" }, "user": { "name": "Name", diff --git a/frontend/appflowy_flutter/lib/plugins/document/application/document_data_pb_extension.dart b/frontend/appflowy_flutter/lib/plugins/document/application/document_data_pb_extension.dart index 85b7bf1d59..67767939da 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/application/document_data_pb_extension.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/application/document_data_pb_extension.dart @@ -91,7 +91,6 @@ extension DocumentDataPBFromTo on DocumentDataPB { class _BackendKeys { const _BackendKeys._(); - static const String page = 'page'; static const String text = 'text'; } @@ -109,7 +108,6 @@ extension BlockToNode on BlockPB { String _typeAdapter(String ty) { final adapter = { - _BackendKeys.page: 'document', _BackendKeys.text: ParagraphBlockKeys.type, }; return adapter[ty] ?? ty; @@ -149,7 +147,6 @@ extension NodeToBlock on Node { String _typeAdapter(String type) { final adapter = { - 'document': 'page', 'paragraph': 'text', }; return adapter[type] ?? type; diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart index 0722ed0e36..f2e8f10afa 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart @@ -114,6 +114,7 @@ class _AppFlowyEditorPageState extends State { maxWidth: double.infinity, ), child: FloatingToolbar( + style: styleCustomizer.floatingToolbarStyleBuilder(), items: toolbarItems, editorState: widget.editorState, scrollController: scrollController, @@ -127,16 +128,16 @@ class _AppFlowyEditorPageState extends State { final standardActions = [ OptionAction.delete, OptionAction.duplicate, - OptionAction.divider, - OptionAction.moveUp, - OptionAction.moveDown, + // OptionAction.divider, + // OptionAction.moveUp, + // OptionAction.moveDown, ]; final configuration = BlockComponentConfiguration( padding: (_) => const EdgeInsets.symmetric(vertical: 4.0), ); final customBlockComponentBuilderMap = { - 'document': DocumentComponentBuilder(), + PageBlockKeys.type: PageBlockComponentBuilder(), ParagraphBlockKeys.type: TextBlockComponentBuilder( configuration: configuration, ), @@ -207,7 +208,7 @@ class _AppFlowyEditorPageState extends State { // customize the action builder. actually, we can customize them in their own builder. Put them here just for convenience. for (final entry in builders.entries) { - if (entry.key == 'document') { + if (entry.key == PageBlockKeys.type) { continue; } final builder = entry.value; @@ -224,7 +225,7 @@ class _AppFlowyEditorPageState extends State { ]; final colorAction = [ - OptionAction.divider, + // OptionAction.divider, OptionAction.color, ]; diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/block_action_button.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/block_action_button.dart index a3ad6b8cee..4b9db59375 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/block_action_button.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/block_action_button.dart @@ -29,7 +29,7 @@ class BlockActionButton extends StatelessWidget { behavior: HitTestBehavior.deferToChild, child: svgWidget( svgName, - size: const Size.square(14.0), + size: const Size.square(18.0), color: Theme.of(context).iconTheme.color, ), ), diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/block_action_option_button.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/block_action_option_button.dart index d55ad9f6ed..e1dafe214a 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/block_action_option_button.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/block_action_option_button.dart @@ -120,9 +120,6 @@ class BlockOptionButton extends StatelessWidget { transaction.moveNode(node.path.next.next, node); break; case OptionAction.color: - // show the color picker - - break; case OptionAction.divider: throw UnimplementedError(); } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/option_action.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/option_action.dart index 765dcb25f0..403b7cd709 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/option_action.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/option_action.dart @@ -2,10 +2,11 @@ import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/ import 'package:appflowy/plugins/database_view/widgets/row/cells/select_option_cell/extension.dart'; import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:appflowy_editor/appflowy_editor.dart' hide FlowySvg; import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:flowy_infra/image.dart'; import 'package:flutter/material.dart'; +import 'package:styled_widget/styled_widget.dart'; enum OptionAction { delete, @@ -36,10 +37,10 @@ class ColorOptionAction extends PopoverActionCell { @override Widget? leftIcon(Color iconColor) { - return svgWidget( - 'editor/color_formatter', - color: iconColor, - ); + return const FlowySvg( + name: 'editor/color_formatter', + size: Size.square(12), + ).padding(all: 2.0); } @override @@ -125,18 +126,13 @@ class OptionActionWrapper extends ActionCell { case OptionAction.moveDown: name = 'editor/move_down'; break; - case OptionAction.color: - throw UnimplementedError(); - case OptionAction.divider: + default: throw UnimplementedError(); } if (name.isEmpty) { return null; } - return svgWidget( - name, - color: iconColor, - ); + return FlowySvg(name: name); } @override diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/option_action_button.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/option_action_button.dart index cca05e45ae..c1faca11a7 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/option_action_button.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/option_action_button.dart @@ -103,9 +103,6 @@ class OptionActionList extends StatelessWidget { transaction.moveNode(node.path.next.next, node); break; case OptionAction.color: - // show the color picker - - break; case OptionAction.divider: throw UnimplementedError(); } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/migration/editor_migration.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/migration/editor_migration.dart index 82bab6e40c..3918a2891e 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/migration/editor_migration.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/migration/editor_migration.dart @@ -37,12 +37,12 @@ class EditorMigration { (element) => element.id == 'cover', ); if (coverNode != null) { - node = documentNode( + node = pageNode( children: children, attributes: coverNode.attributes, ); } else { - node = documentNode(children: children); + node = pageNode(children: children); } } else if (id == 'callout') { final emoji = nodeV0.attributes['emoji'] ?? '📌'; @@ -141,12 +141,13 @@ class EditorMigration { } const backgroundColor = 'backgroundColor'; if (attributes.containsKey(backgroundColor)) { - attributes['highlightColor'] = attributes[backgroundColor]; + attributes[FlowyRichTextKeys.highlightColor] = + attributes[backgroundColor]; attributes.remove(backgroundColor); } const color = 'color'; if (attributes.containsKey(color)) { - attributes['textColor'] = attributes[color]; + attributes[FlowyRichTextKeys.textColor] = attributes[color]; attributes.remove(color); } return attributes; diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/openai/widgets/auto_completion_node_widget.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/openai/widgets/auto_completion_node_widget.dart index 025ee5480d..eb36767e80 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/openai/widgets/auto_completion_node_widget.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/openai/widgets/auto_completion_node_widget.dart @@ -375,7 +375,7 @@ class _AutoCompletionBlockComponentState final previous = widget.node.previous; final Selection selection; if (previous == null || - previous.type != 'paragraph' || + previous.type != ParagraphBlockKeys.type || (previous.delta?.toPlainText().isNotEmpty ?? false)) { selection = Selection.single( path: widget.node.path, diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_style.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_style.dart index cdd34efd26..bd653bbe13 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_style.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_style.dart @@ -136,4 +136,11 @@ class EditorStyleCustomizer { selectionMenuItemSelectedColor: theme.hoverColor, ); } + + FloatingToolbarStyle floatingToolbarStyleBuilder() { + final theme = Theme.of(context); + return FloatingToolbarStyle( + backgroundColor: theme.cardColor, + ); + } } diff --git a/frontend/appflowy_flutter/lib/workspace/application/settings/settings_file_exporter_cubit.dart b/frontend/appflowy_flutter/lib/workspace/application/settings/settings_file_exporter_cubit.dart index 62073d1561..8e30186de7 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/settings/settings_file_exporter_cubit.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/settings/settings_file_exporter_cubit.dart @@ -8,6 +8,20 @@ class SettingsFileExportState { initialize(); } + List get selectedViews { + final selectedViews = []; + for (var i = 0; i < views.length; i++) { + if (selectedApps[i]) { + for (var j = 0; j < views[i].childViews.length; j++) { + if (selectedItems[i][j]) { + selectedViews.add(views[i].childViews[j]); + } + } + } + } + return selectedViews; + } + List views; List expanded = []; List selectedApps = []; diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_export_file_widget.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_export_file_widget.dart new file mode 100644 index 0000000000..9421292b98 --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_export_file_widget.dart @@ -0,0 +1,73 @@ +import 'package:appflowy/workspace/presentation/settings/widgets/settings_file_exporter_widget.dart'; +import 'package:flowy_infra/image.dart'; +import 'package:flutter/material.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; + +import '../../../../generated/locale_keys.g.dart'; + +class SettingsExportFileWidget extends StatefulWidget { + const SettingsExportFileWidget({ + super.key, + }); + + @override + State createState() => + SettingsExportFileWidgetState(); +} + +@visibleForTesting +class SettingsExportFileWidgetState extends State { + @override + Widget build(BuildContext context) { + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + FlowyText.medium( + LocaleKeys.settings_files_exportData.tr(), + overflow: TextOverflow.ellipsis, + ), + const Spacer(), + _OpenExportedDirectoryButton( + onTap: () async { + await showDialog( + context: context, + builder: (context) { + return const FlowyDialog( + child: Padding( + padding: EdgeInsets.symmetric( + horizontal: 16, + vertical: 20, + ), + child: FileExporterWidget(), + ), + ); + }, + ); + }, + ), + ], + ); + } +} + +class _OpenExportedDirectoryButton extends StatelessWidget { + const _OpenExportedDirectoryButton({ + required this.onTap, + }); + + final VoidCallback onTap; + + @override + Widget build(BuildContext context) { + return FlowyIconButton( + hoverColor: Theme.of(context).colorScheme.secondaryContainer, + tooltipText: LocaleKeys.settings_files_open.tr(), + icon: svgWidget( + 'common/open_folder', + color: Theme.of(context).iconTheme.color, + ), + onPressed: onTap, + ); + } +} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_file_exporter_widget.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_file_exporter_widget.dart index 1dbd7a71ea..6f1b2c5e26 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_file_exporter_widget.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_file_exporter_widget.dart @@ -1,15 +1,22 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:appflowy/plugins/document/application/document_data_pb_extension.dart'; +import 'package:appflowy/plugins/document/application/prelude.dart'; import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/util/file_picker/file_picker_service.dart'; import 'package:appflowy/workspace/application/settings/settings_file_exporter_cubit.dart'; +import 'package:appflowy_backend/log.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart'; import 'package:dartz/dartz.dart' as dartz; import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart' hide WidgetBuilder; import 'package:appflowy_backend/dispatch/dispatch.dart'; import 'package:appflowy_backend/protobuf/flowy-error/errors.pbserver.dart'; import 'package:appflowy_backend/protobuf/flowy-folder2/workspace.pb.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; - +import 'package:path/path.dart' as p; import '../../../../generated/locale_keys.g.dart'; class FileExporterWidget extends StatefulWidget { @@ -22,24 +29,44 @@ class FileExporterWidget extends StatefulWidget { class _FileExporterWidgetState extends State { // Map> _selectedPages = {}; + SettingsFileExporterCubit? cubit; + @override Widget build(BuildContext context) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - FlowyText.medium( - LocaleKeys.settings_files_selectFiles.tr(), - fontSize: 16.0, - ), - const VSpace(8), - Expanded(child: _buildFileSelector(context)), - const VSpace(8), - _buildButtons(context) - ], + return FutureBuilder>( + future: FolderEventReadCurrentWorkspace().send(), + builder: (context, snapshot) { + if (snapshot.hasData && + snapshot.connectionState == ConnectionState.done) { + final workspaces = snapshot.data?.getLeftOrNull(); + if (workspaces != null) { + final views = workspaces.workspace.views; + cubit ??= SettingsFileExporterCubit(views: views); + return BlocProvider.value( + value: cubit!, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + FlowyText.medium( + LocaleKeys.settings_files_selectFiles.tr(), + fontSize: 16.0, + ), + const VSpace(8), + const Expanded(child: _ExpandedList()), + const VSpace(8), + _buildButtons() + ], + ), + ); + } + } + return const CircularProgressIndicator(); + }, ); } - Row _buildButtons(BuildContext context) { + Widget _buildButtons() { return Row( children: [ const Spacer(), @@ -55,8 +82,28 @@ class _FileExporterWidgetState extends State { onPressed: () async { await getIt() .getDirectoryPath() - .then((exportPath) { - Navigator.of(context).pop(); + .then((exportPath) async { + if (exportPath != null && cubit != null) { + final views = cubit!.state.selectedViews; + final result = + await _AppFlowyFileExporter.exportToPath(exportPath, views); + if (result.$1) { + // success + _showToast(LocaleKeys.settings_files_exportFileSuccess.tr()); + } else { + _showToast( + LocaleKeys.settings_files_exportFileFail.tr() + + result.$2.join('\n'), + ); + } + } else { + _showToast(LocaleKeys.settings_files_exportFileFail.tr()); + } + if (mounted) { + Navigator.of(context).popUntil( + (router) => router.settings.name == '/', + ); + } }); }, ), @@ -64,24 +111,14 @@ class _FileExporterWidgetState extends State { ); } - FutureBuilder> - _buildFileSelector(BuildContext context) { - return FutureBuilder>( - future: FolderEventReadCurrentWorkspace().send(), - builder: (context, snapshot) { - if (snapshot.hasData && - snapshot.connectionState == ConnectionState.done) { - final workspaces = snapshot.data?.getLeftOrNull(); - if (workspaces != null) { - final views = workspaces.workspace.views; - return BlocProvider( - create: (_) => SettingsFileExporterCubit(views: views), - child: const _ExpandedList(), - ); - } - } - return const CircularProgressIndicator(); - }, + void _showToast(String message) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: FlowyText( + message, + color: Theme.of(context).colorScheme.onSurface, + ), + ), ); } } @@ -131,9 +168,9 @@ class _ExpandedListState extends State<_ExpandedList> { final apps = state.views; final expanded = state.expanded; final selectedItems = state.selectedItems; - final isExpaned = expanded[index] == true; + final isExpanded = expanded[index] == true; List expandedChildren = []; - if (isExpaned) { + if (isExpanded) { for (var i = 0; i < selectedItems[index].length; i++) { final name = apps[index].childViews[i].name; final checkbox = CheckboxListTile( @@ -160,7 +197,7 @@ class _ExpandedListState extends State<_ExpandedList> { child: ListTile( title: FlowyText.medium(apps[index].name), trailing: Icon( - isExpaned + isExpanded ? Icons.arrow_drop_down_rounded : Icons.arrow_drop_up_rounded, ), @@ -182,3 +219,45 @@ extension AppFlowy on dartz.Either { return null; } } + +class _AppFlowyFileExporter { + static Future<(bool result, List failedNames)> exportToPath( + String path, + List views, + ) async { + final failedFileNames = []; + final Map names = {}; + final documentService = DocumentService(); + for (final view in views) { + String? content; + String? fileExtension; + switch (view.layout) { + case ViewLayoutPB.Document: + final document = await documentService.openDocument(view: view); + document.fold((l) => Log.error(l), (r) { + final json = r.toDocument()?.toJson(); + if (json != null) { + content = jsonEncode(json); + } + }); + fileExtension = 'afdocument'; + break; + default: + // TODO(nathan): export the new databse data to json + content = null; + break; + } + if (content != null) { + final count = names.putIfAbsent(view.name, () => 0); + final name = count == 0 ? view.name : '${view.name}($count)'; + final file = File(p.join(path, '$name.$fileExtension')); + await file.writeAsString(content!); + names[view.name] = count + 1; + } else { + failedFileNames.add(view.name); + } + } + + return (failedFileNames.isEmpty, failedFileNames); + } +} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_file_system_view.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_file_system_view.dart index 507efbe37b..6cf682503f 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_file_system_view.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_file_system_view.dart @@ -1,3 +1,4 @@ +import 'package:appflowy/workspace/presentation/settings/widgets/settings_export_file_widget.dart'; import 'package:appflowy/workspace/presentation/settings/widgets/settings_file_customize_location_view.dart'; import 'package:flutter/material.dart'; @@ -11,22 +12,18 @@ class SettingsFileSystemView extends StatefulWidget { } class _SettingsFileSystemViewState extends State { + late final _items = [ + const SettingsFileLocationCustomizer(), + const SettingsExportFileWidget() + ]; + @override Widget build(BuildContext context) { - // return Column( - // children: [], - // ); return ListView.separated( - itemBuilder: (context, index) { - if (index == 0) { - return const SettingsFileLocationCustomizer(); - } else if (index == 1) { - // return _buildExportDatabaseButton(); - } - return Container(); - }, + shrinkWrap: true, + itemBuilder: (context, index) => _items[index], separatorBuilder: (context, index) => const Divider(), - itemCount: 2, // make the divider taking effect. + itemCount: _items.length, ); } } diff --git a/frontend/appflowy_flutter/pubspec.lock b/frontend/appflowy_flutter/pubspec.lock index c4659fdbe5..3b0266b0cd 100644 --- a/frontend/appflowy_flutter/pubspec.lock +++ b/frontend/appflowy_flutter/pubspec.lock @@ -52,12 +52,11 @@ packages: appflowy_editor: dependency: "direct main" description: - path: "." - ref: "9732d30" - resolved-ref: "9732d30e852ccb832785d6fff3923966452ffcf4" - url: "https://github.com/AppFlowy-IO/appflowy-editor.git" - source: git - version: "0.1.12" + name: appflowy_editor + sha256: "3561bd7bd99541508353034130a98ab2d9be54f690bb982f85c2b3eedb8fe63e" + url: "https://pub.dev" + source: hosted + version: "1.0.0-dev.1" appflowy_popover: dependency: "direct main" description: diff --git a/frontend/appflowy_flutter/pubspec.yaml b/frontend/appflowy_flutter/pubspec.yaml index f430567d5f..0e5daf54ad 100644 --- a/frontend/appflowy_flutter/pubspec.yaml +++ b/frontend/appflowy_flutter/pubspec.yaml @@ -15,7 +15,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. # Read more about iOS versioning at # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -version: 0.1.5 +version: 0.2.0 environment: sdk: ">=3.0.0 <4.0.0" @@ -42,10 +42,7 @@ dependencies: git: url: https://github.com/AppFlowy-IO/appflowy-board.git ref: a183c57 - appflowy_editor: - git: - url: https://github.com/AppFlowy-IO/appflowy-editor.git - ref: 9732d30 + appflowy_editor: 1.0.0-dev.1 appflowy_popover: path: packages/appflowy_popover diff --git a/frontend/appflowy_flutter/test/unit_test/editor/editor_migration_test.dart b/frontend/appflowy_flutter/test/unit_test/editor/editor_migration_test.dart index 86c57131ab..9fe1f7bc24 100644 --- a/frontend/appflowy_flutter/test/unit_test/editor/editor_migration_test.dart +++ b/frontend/appflowy_flutter/test/unit_test/editor/editor_migration_test.dart @@ -1,9 +1,20 @@ +import 'dart:convert'; + +import 'package:appflowy/plugins/document/presentation/editor_plugins/migration/editor_migration.dart'; +import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + group('editor migration, from v0.1.x to 0.2', () { - test('migrate readme', () { - // final + test('migrate readme', () async { + final readme = await rootBundle.loadString('assets/template/readme.json'); + final oldDocument = DocumentV0.fromJson(json.decode(readme)); + final document = EditorMigration.migrateDocument(readme); + expect(document.root.type, 'page'); + expect(oldDocument.root.children.length, document.root.children.length); }); }); } diff --git a/frontend/appflowy_flutter/test/unit_test/editor/share_markdown_test.dart b/frontend/appflowy_flutter/test/unit_test/editor/share_markdown_test.dart index a374a5566c..6d39c5a0c1 100644 --- a/frontend/appflowy_flutter/test/unit_test/editor/share_markdown_test.dart +++ b/frontend/appflowy_flutter/test/unit_test/editor/share_markdown_test.dart @@ -12,11 +12,11 @@ void main() { const text = ''' { "document":{ - "type":"document", + "type":"page", "children":[ { "type":"math_equation", - "attributes":{ + "data":{ "math_equation":"E = MC^2" } } @@ -40,11 +40,11 @@ void main() { const text = ''' { "document":{ - "type":"editor", + "type":"page", "children":[ { "type":"code_block", - "attributes":{ + "data":{ "code_block":"Some Code" } } @@ -67,7 +67,7 @@ void main() { const text = ''' { "document":{ - "type":"editor", + "type":"page", "children":[ { "type":"divider" diff --git a/frontend/rust-lib/Cargo.lock b/frontend/rust-lib/Cargo.lock index 8220d0f893..0ff7929f02 100644 --- a/frontend/rust-lib/Cargo.lock +++ b/frontend/rust-lib/Cargo.lock @@ -1692,6 +1692,7 @@ dependencies = [ "flowy-derive", "flowy-error", "flowy-notification", + "indexmap", "lib-dispatch", "nanoid", "parking_lot 0.12.1", diff --git a/frontend/rust-lib/flowy-core/assets/read_me.json b/frontend/rust-lib/flowy-core/assets/read_me.json new file mode 100644 index 0000000000..2e47a80467 --- /dev/null +++ b/frontend/rust-lib/flowy-core/assets/read_me.json @@ -0,0 +1,219 @@ +{ + "type": "page", + "children": [ + { + "type": "heading", + "data": { "delta": [{ "insert": "Welcome to AppFlowy!" }], "level": 1 } + }, + { + "type": "heading", + "data": { "delta": [{ "insert": "Here are the basics" }], "level": 2 } + }, + { + "type": "todo_list", + "data": { + "delta": [{ "insert": "Click anywhere and just start typing." }], + "checked": false + } + }, + { + "type": "todo_list", + "data": { + "checked": false, + "delta": [ + { + "attributes": { "bg_color": "0x4dffeb3b" }, + "insert": "Highlight " + }, + { "insert": "any text, and use the editing menu to " }, + { "attributes": { "italic": true }, "insert": "style" }, + { "insert": " " }, + { "attributes": { "bold": true }, "insert": "your" }, + { "insert": " " }, + { "attributes": { "underline": true }, "insert": "writing" }, + { "insert": " " }, + { "attributes": { "code": true }, "insert": "however" }, + { "insert": " you " }, + { "attributes": { "strikethrough": true }, "insert": "like." } + ] + } + }, + { + "type": "todo_list", + "data": { + "checked": false, + "delta": [ + { "insert": "As soon as you type " }, + { + "attributes": { "code": true, "font_color": "0xff00b5ff" }, + "insert": "/" + }, + { "insert": " a menu will pop up. Select " }, + { + "attributes": { "bg_color": "0x4d9c27b0" }, + "insert": "different types" + }, + { "insert": " of content blocks you can add." } + ] + } + }, + { + "type": "todo_list", + "data": { + "delta": [ + { "insert": "Type " }, + { "attributes": { "code": true }, "insert": "/" }, + { "insert": " followed by " }, + { "attributes": { "code": true }, "insert": "/bullet" }, + { "insert": " or " }, + { "attributes": { "code": true }, "insert": "/num" }, + { "attributes": { "code": false }, "insert": " to create a list." } + ], + "checked": false + } + }, + { + "type": "todo_list", + "data": { + "delta": [ + { "insert": "Click " }, + { "attributes": { "code": true }, "insert": "+ New Page " }, + { + "insert": "button at the bottom of your sidebar to add a new page." + } + ], + "checked": true + } + }, + { + "type": "todo_list", + "data": { + "checked": false, + "delta": [ + { "insert": "Click " }, + { "attributes": { "code": true }, "insert": "+" }, + { "insert": " next to any page title in the sidebar to " }, + { + "attributes": { "font_color": "0xff8427e0" }, + "insert": "quickly" + }, + { "insert": " add a new subpage, " }, + { "attributes": { "code": true }, "insert": "Document" }, + { "attributes": { "code": false }, "insert": ", " }, + { "attributes": { "code": true }, "insert": "Grid" }, + { "attributes": { "code": false }, "insert": ", or " }, + { "attributes": { "code": true }, "insert": "Kanban Board" }, + { "attributes": { "code": false }, "insert": "." } + ] + } + }, + { "type": "paragraph", "data": { "delta": [] } }, + { "type": "divider" }, + { "type": "paragraph", "data": { "delta": [] } }, + { + "type": "heading", + "data": { + "delta": [{ "insert": "Keyboard shortcuts, markdown, and code block" }], + "level": 2 + } + }, + { + "type": "numbered_list", + "data": { + "delta": [ + { "insert": "Keyboard shortcuts " }, + { + "attributes": { + "href": "https://appflowy.gitbook.io/docs/essential-documentation/shortcuts" + }, + "insert": "guide" + } + ] + } + }, + { + "type": "numbered_list", + "data": { + "delta": [ + { "insert": "Markdown " }, + { + "attributes": { + "href": "https://appflowy.gitbook.io/docs/essential-documentation/markdown" + }, + "insert": "reference" + } + ] + } + }, + { + "type": "numbered_list", + "data": { + "delta": [ + { "insert": "Type " }, + { "attributes": { "code": true }, "insert": "/code" }, + { + "attributes": { "code": false }, + "insert": " to insert a code block" + } + ] + } + }, + { + "type": "code", + "data": { + "language": "rust", + "delta": [ + { + "insert": "// This is the main function.\nfn main() {\n // Print text to the console.\n println!(\"Hello World!\");\n}" + } + ] + } + }, + { "type": "paragraph", "data": { "delta": [] } }, + { + "type": "heading", + "data": { "level": 2, "delta": [{ "insert": "Have a question❓" }] } + }, + { + "type": "quote", + "data": { + "delta": [ + { "insert": "Click " }, + { "attributes": { "code": true }, "insert": "?" }, + { "insert": " at the bottom right for help and support." } + ] + } + }, + { "type": "paragraph", "data": { "delta": [] } }, + { + "type": "callout", + "data": { + "bgColor": "#F0F0F0", + "delta": [ + { "insert": "\nLike AppFlowy? Follow us:\n" }, + { + "attributes": { + "href": "https://github.com/AppFlowy-IO/AppFlowy" + }, + "insert": "GitHub" + }, + { "insert": "\n" }, + { + "attributes": { "href": "https://twitter.com/appflowy" }, + "insert": "Twitter" + }, + { "insert": ": @appflowy\n" }, + { + "attributes": { "href": "https://blog-appflowy.ghost.io/" }, + "insert": "Newsletter" + }, + { "insert": "\n" } + ], + "icon": "🥰" + } + }, + { "type": "paragraph", "data": { "delta": [] } }, + { "type": "paragraph", "data": { "delta": [] } }, + { "type": "paragraph", "data": { "delta": [] } } + ] +} diff --git a/frontend/rust-lib/flowy-core/src/deps_resolve/folder2_deps.rs b/frontend/rust-lib/flowy-core/src/deps_resolve/folder2_deps.rs index b2b6c26778..5022401583 100644 --- a/frontend/rust-lib/flowy-core/src/deps_resolve/folder2_deps.rs +++ b/frontend/rust-lib/flowy-core/src/deps_resolve/folder2_deps.rs @@ -13,6 +13,7 @@ use flowy_database2::DatabaseManager2; use flowy_document2::document_data::DocumentDataWrapper; use flowy_document2::entities::DocumentDataPB; use flowy_document2::manager::DocumentManager; +use flowy_document2::parser::json::parser::JsonToDocumentParser; use flowy_error::FlowyError; use flowy_folder2::deps::{FolderCloudService, FolderUser}; use flowy_folder2::entities::ViewLayoutPB; @@ -113,7 +114,6 @@ impl FolderOperationHandler for DocumentFolderOperation { _ext: HashMap, ) -> FutureResult<(), FlowyError> { debug_assert_eq!(layout, ViewLayout::Document); - // TODO: implement read the document data from custom data. let view_id = view_id.to_string(); let manager = self.0.clone(); FutureResult::new(async move { @@ -134,11 +134,12 @@ impl FolderOperationHandler for DocumentFolderOperation { ) -> FutureResult<(), FlowyError> { debug_assert_eq!(layout, ViewLayout::Document); + let json_str = include_str!("../../assets/read_me.json"); let view_id = view_id.to_string(); let manager = self.0.clone(); - // TODO: implement read the document data from json. FutureResult::new(async move { - manager.create_document(view_id, DocumentDataWrapper::default())?; + let document_pb = JsonToDocumentParser::json_str_to_document(json_str)?; + manager.create_document(view_id, document_pb.into())?; Ok(()) }) } diff --git a/frontend/rust-lib/flowy-database/src/services/export.rs b/frontend/rust-lib/flowy-database/src/services/export.rs new file mode 100644 index 0000000000..1884a7298a --- /dev/null +++ b/frontend/rust-lib/flowy-database/src/services/export.rs @@ -0,0 +1,182 @@ +use crate::entities::FieldType; + +use crate::services::cell::TypeCellData; +use crate::services::database::DatabaseEditor; +use crate::services::field::{ + CheckboxTypeOptionPB, ChecklistTypeOptionPB, DateCellData, DateTypeOptionPB, + MultiSelectTypeOptionPB, NumberTypeOptionPB, RichTextTypeOptionPB, SingleSelectTypeOptionPB, + URLCellData, +}; +use database_model::{FieldRevision, TypeOptionDataDeserializer}; +use flowy_error::{FlowyError, FlowyResult}; +use indexmap::IndexMap; +use serde::Serialize; +use serde_json::{json, Map, Value}; +use std::collections::HashMap; + +use std::sync::Arc; + +#[derive(Debug, Clone, Serialize)] +pub struct ExportField { + pub id: String, + pub name: String, + pub field_type: i64, + pub visibility: bool, + pub width: i64, + pub type_options: HashMap, + pub is_primary: bool, +} + +#[derive(Debug, Clone, Serialize)] +struct ExportCell { + data: String, + field_type: FieldType, +} + +impl From<&Arc> for ExportField { + fn from(field_rev: &Arc) -> Self { + let field_type = FieldType::from(field_rev.ty); + let mut type_options: HashMap = HashMap::new(); + + field_rev + .type_options + .iter() + .filter(|(k, _)| k == &&field_rev.ty.to_string()) + .for_each(|(k, s)| { + let value = match field_type { + FieldType::RichText => { + let pb = RichTextTypeOptionPB::from_json_str(s); + serde_json::to_value(pb).unwrap() + }, + FieldType::Number => { + let pb = NumberTypeOptionPB::from_json_str(s); + let mut map = Map::new(); + map.insert("format".to_string(), json!(pb.format as u8)); + map.insert("scale".to_string(), json!(pb.scale)); + map.insert("symbol".to_string(), json!(pb.symbol)); + map.insert("name".to_string(), json!(pb.name)); + Value::Object(map) + }, + FieldType::DateTime => { + let pb = DateTypeOptionPB::from_json_str(s); + let mut map = Map::new(); + map.insert("date_format".to_string(), json!(pb.date_format as u8)); + map.insert("time_format".to_string(), json!(pb.time_format as u8)); + map.insert("field_type".to_string(), json!(FieldType::DateTime as u8)); + Value::Object(map) + }, + FieldType::SingleSelect => { + let pb = SingleSelectTypeOptionPB::from_json_str(s); + let value = serde_json::to_string(&pb).unwrap(); + let mut map = Map::new(); + map.insert("content".to_string(), Value::String(value)); + Value::Object(map) + }, + FieldType::MultiSelect => { + let pb = MultiSelectTypeOptionPB::from_json_str(s); + let value = serde_json::to_string(&pb).unwrap(); + let mut map = Map::new(); + map.insert("content".to_string(), Value::String(value)); + Value::Object(map) + }, + FieldType::Checkbox => { + let pb = CheckboxTypeOptionPB::from_json_str(s); + serde_json::to_value(pb).unwrap() + }, + FieldType::URL => { + let pb = RichTextTypeOptionPB::from_json_str(s); + serde_json::to_value(pb).unwrap() + }, + FieldType::Checklist => { + let pb = ChecklistTypeOptionPB::from_json_str(s); + let value = serde_json::to_string(&pb).unwrap(); + let mut map = Map::new(); + map.insert("content".to_string(), Value::String(value)); + Value::Object(map) + }, + }; + type_options.insert(k.clone(), value); + }); + Self { + id: field_rev.id.clone(), + name: field_rev.name.clone(), + field_type: field_rev.ty as i64, + visibility: true, + width: 100, + type_options, + is_primary: field_rev.is_primary, + } + } +} + +pub struct CSVExport; +impl CSVExport { + pub async fn export_database( + &self, + view_id: &str, + database_editor: &Arc, + ) -> FlowyResult { + let mut wtr = csv::Writer::from_writer(vec![]); + let row_revs = database_editor.get_all_row_revs(view_id).await?; + let field_revs = database_editor.get_field_revs(None).await?; + + // Write fields + let field_records = field_revs + .iter() + .map(|field| ExportField::from(field)) + .map(|field| serde_json::to_string(&field).unwrap()) + .collect::>(); + + wtr + .write_record(&field_records) + .map_err(|e| FlowyError::internal().context(e))?; + + // Write rows + let mut field_by_field_id = IndexMap::new(); + field_revs.into_iter().for_each(|field| { + field_by_field_id.insert(field.id.clone(), field); + }); + for row_rev in row_revs { + let cells = field_by_field_id + .iter() + .map(|(field_id, field)| { + let field_type = FieldType::from(field.ty); + let data = row_rev + .cells + .get(field_id) + .map(|cell| TypeCellData::try_from(cell)) + .map(|data| { + data + .map(|data| match field_type { + FieldType::DateTime => { + match serde_json::from_str::(&data.cell_str) { + Ok(cell_data) => cell_data.timestamp.unwrap_or_default().to_string(), + Err(_) => "".to_string(), + } + }, + FieldType::URL => match serde_json::from_str::(&data.cell_str) { + Ok(cell_data) => cell_data.content, + Err(_) => "".to_string(), + }, + _ => data.cell_str, + }) + .unwrap_or_default() + }) + .unwrap_or_else(|| "".to_string()); + let cell = ExportCell { data, field_type }; + serde_json::to_string(&cell).unwrap() + }) + .collect::>(); + + if let Err(e) = wtr.write_record(&cells) { + tracing::warn!("CSV failed to write record: {}", e); + } + } + + let data = wtr + .into_inner() + .map_err(|e| FlowyError::internal().context(e))?; + let csv = String::from_utf8(data).map_err(|e| FlowyError::internal().context(e))?; + Ok(csv) + } +} diff --git a/frontend/rust-lib/flowy-document2/Cargo.toml b/frontend/rust-lib/flowy-document2/Cargo.toml index d861d95651..6f8fa74dbb 100644 --- a/frontend/rust-lib/flowy-document2/Cargo.toml +++ b/frontend/rust-lib/flowy-document2/Cargo.toml @@ -26,6 +26,7 @@ serde_json = {version = "1.0"} tracing = { version = "0.1", features = ["log"] } tokio = { version = "1.26", features = ["full"] } anyhow = "1.0" +indexmap = {version = "1.9.2", features = ["serde"]} [dev-dependencies] tempfile = "3.4.0" diff --git a/frontend/rust-lib/flowy-document2/src/entities.rs b/frontend/rust-lib/flowy-document2/src/entities.rs index ba62346227..ae86d04bab 100644 --- a/frontend/rust-lib/flowy-document2/src/entities.rs +++ b/frontend/rust-lib/flowy-document2/src/entities.rs @@ -39,7 +39,7 @@ pub struct GetDocumentDataPayloadPB { // Support customize initial data } -#[derive(Default, ProtoBuf)] +#[derive(Default, Debug, ProtoBuf)] pub struct DocumentDataPB { #[pb(index = 1)] pub page_id: String, @@ -69,13 +69,13 @@ pub struct BlockPB { pub children_id: String, } -#[derive(Default, ProtoBuf)] +#[derive(Default, ProtoBuf, Debug)] pub struct MetaPB { #[pb(index = 1)] pub children_map: HashMap, } -#[derive(Default, ProtoBuf)] +#[derive(Default, ProtoBuf, Debug)] pub struct ChildrenPB { #[pb(index = 1)] pub children: Vec, diff --git a/frontend/rust-lib/flowy-document2/src/lib.rs b/frontend/rust-lib/flowy-document2/src/lib.rs index d991cf96ff..0b6ec6b5fd 100644 --- a/frontend/rust-lib/flowy-document2/src/lib.rs +++ b/frontend/rust-lib/flowy-document2/src/lib.rs @@ -3,6 +3,7 @@ pub mod document_data; pub mod entities; pub mod event_map; pub mod manager; +pub mod parser; pub mod protobuf; mod event_handler; diff --git a/frontend/rust-lib/flowy-document2/src/parser/json/block.rs b/frontend/rust-lib/flowy-document2/src/parser/json/block.rs new file mode 100644 index 0000000000..16b3bd0c73 --- /dev/null +++ b/frontend/rust-lib/flowy-document2/src/parser/json/block.rs @@ -0,0 +1,20 @@ +use std::collections::HashMap; + +use serde::{Deserialize, Serialize}; +use serde_json::Value; + +/// Json format: +/// { +/// 'type': string, +/// 'data': Map +/// 'children': [Block], +/// } +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct Block { + #[serde(rename = "type")] + pub ty: String, + #[serde(default)] + pub data: HashMap, + #[serde(default)] + pub children: Vec, +} diff --git a/frontend/rust-lib/flowy-document2/src/parser/json/mod.rs b/frontend/rust-lib/flowy-document2/src/parser/json/mod.rs new file mode 100644 index 0000000000..edbc33f64c --- /dev/null +++ b/frontend/rust-lib/flowy-document2/src/parser/json/mod.rs @@ -0,0 +1,2 @@ +pub mod block; +pub mod parser; diff --git a/frontend/rust-lib/flowy-document2/src/parser/json/parser.rs b/frontend/rust-lib/flowy-document2/src/parser/json/parser.rs new file mode 100644 index 0000000000..c8c6e714e4 --- /dev/null +++ b/frontend/rust-lib/flowy-document2/src/parser/json/parser.rs @@ -0,0 +1,81 @@ +use std::{collections::HashMap, vec}; + +use flowy_error::FlowyResult; +use indexmap::IndexMap; +use nanoid::nanoid; + +use crate::entities::{BlockPB, ChildrenPB, DocumentDataPB, MetaPB}; + +use super::block::Block; + +pub struct JsonToDocumentParser; + +impl JsonToDocumentParser { + pub fn json_str_to_document(json_str: &str) -> FlowyResult { + let root = serde_json::from_str::(json_str)?; + + let page_id = nanoid!(10); + + // generate the blocks + // the root's parent id is empty + let blocks = Self::generate_blocks(&root, Some(page_id.clone()), "".to_string()); + + // generate the children map + let children_map = Self::generate_children_map(&blocks); + + Ok(DocumentDataPB { + page_id, + blocks: blocks.into_iter().collect(), + meta: MetaPB { children_map }, + }) + } + + fn generate_blocks( + block: &Block, + id: Option, + parent_id: String, + ) -> IndexMap { + let block_pb = Self::block_to_block_pb(block, id, parent_id); + let mut blocks = IndexMap::new(); + for child in &block.children { + let child_blocks = Self::generate_blocks(child, None, block_pb.id.clone()); + blocks.extend(child_blocks); + } + blocks.insert(block_pb.id.clone(), block_pb); + blocks + } + + fn generate_children_map(blocks: &IndexMap) -> HashMap { + let mut children_map = HashMap::new(); + for (id, block) in blocks.iter() { + // add itself to it's parent's children + if block.parent_id.is_empty() { + continue; + } + let parent_block = blocks.get(&block.parent_id); + if let Some(parent_block) = parent_block { + // insert itself to it's parent's children + let children_pb = children_map + .entry(parent_block.children_id.clone()) + .or_insert_with(|| ChildrenPB { children: vec![] }); + children_pb.children.push(id.clone()); + // create a children map entry for itself + children_map + .entry(block.children_id.clone()) + .or_insert_with(|| ChildrenPB { children: vec![] }); + } + } + children_map + } + + fn block_to_block_pb(block: &Block, id: Option, parent_id: String) -> BlockPB { + let id = id.unwrap_or(nanoid!(10)); + BlockPB { + id: id.clone(), + ty: block.ty.clone(), + data: serde_json::to_string(&block.data).unwrap(), + parent_id: parent_id.clone(), + children_id: nanoid!(10), + } + } +} diff --git a/frontend/rust-lib/flowy-document2/src/parser/mod.rs b/frontend/rust-lib/flowy-document2/src/parser/mod.rs new file mode 100644 index 0000000000..22fdbb38c8 --- /dev/null +++ b/frontend/rust-lib/flowy-document2/src/parser/mod.rs @@ -0,0 +1 @@ +pub mod json; diff --git a/frontend/rust-lib/flowy-document2/tests/main.rs b/frontend/rust-lib/flowy-document2/tests/main.rs index 103318948c..ebd90c2403 100644 --- a/frontend/rust-lib/flowy-document2/tests/main.rs +++ b/frontend/rust-lib/flowy-document2/tests/main.rs @@ -1 +1,2 @@ mod document; +mod parser; diff --git a/frontend/rust-lib/flowy-document2/tests/parser/json/block_test.rs b/frontend/rust-lib/flowy-document2/tests/parser/json/block_test.rs new file mode 100644 index 0000000000..f6ee20bbe7 --- /dev/null +++ b/frontend/rust-lib/flowy-document2/tests/parser/json/block_test.rs @@ -0,0 +1,102 @@ +use flowy_document2::parser::json::block::Block; +use serde_json::json; + +#[test] +fn test_empty_data_and_children() { + let json = json!({ + "type": "page", + }); + let block = serde_json::from_value::(json).unwrap(); + assert_eq!(block.ty, "page"); + assert!(block.data.is_empty()); + assert!(block.children.is_empty()); +} + +#[test] +fn test_data() { + let json = json!({ + "type": "todo_list", + "data": { + "delta": [{ "insert": "Click anywhere and just start typing." }], + "checked": false + } + }); + let block = serde_json::from_value::(json).unwrap(); + assert_eq!(block.ty, "todo_list"); + assert_eq!(block.data.len(), 2); + assert_eq!(block.data.get("checked").unwrap(), false); + assert_eq!( + block.data.get("delta").unwrap().to_owned(), + json!([{ "insert": "Click anywhere and just start typing." }]) + ); + assert!(block.children.is_empty()); +} + +#[test] +fn test_children() { + let json = json!({ + "type": "page", + "children": [ + { + "type": "heading", + "data": { + "delta": [{ "insert": "Welcome to AppFlowy!" }], + "level": 1 + } + }, + { + "type": "todo_list", + "data": { + "delta": [{ "insert": "Welcome to AppFlowy!" }], + "checked": false + } + } + ]}); + let block = serde_json::from_value::(json).unwrap(); + assert!(block.data.is_empty()); + assert_eq!(block.ty, "page"); + assert_eq!(block.children.len(), 2); + // heading + let heading = &block.children[0]; + assert_eq!(heading.ty, "heading"); + assert_eq!(heading.data.len(), 2); + + // todo_list + let todo_list = &block.children[1]; + assert_eq!(todo_list.ty, "todo_list"); + assert_eq!(todo_list.data.len(), 2); +} + +#[test] +fn test_nested_children() { + let json = json!({ + "type": "page", + "children": [ + { + "type": "paragraph", + "children": [ + { + "type": "paragraph", + "children": [ + { + "type": "paragraph", + "children": [ + { + "type": "paragraph" + } + ] + } + ] + } + ] + } + ] + }); + let block = serde_json::from_value::(json).unwrap(); + assert!(block.data.is_empty()); + assert_eq!(block.ty, "page"); + assert_eq!( + block.children[0].children[0].children[0].children[0].ty, + "paragraph" + ); +} diff --git a/frontend/rust-lib/flowy-document2/tests/parser/json/mod.rs b/frontend/rust-lib/flowy-document2/tests/parser/json/mod.rs new file mode 100644 index 0000000000..6eb3f6ce8d --- /dev/null +++ b/frontend/rust-lib/flowy-document2/tests/parser/json/mod.rs @@ -0,0 +1,2 @@ +mod block_test; +mod parser_test; diff --git a/frontend/rust-lib/flowy-document2/tests/parser/json/parser_test.rs b/frontend/rust-lib/flowy-document2/tests/parser/json/parser_test.rs new file mode 100644 index 0000000000..20b10ffd29 --- /dev/null +++ b/frontend/rust-lib/flowy-document2/tests/parser/json/parser_test.rs @@ -0,0 +1,103 @@ +use flowy_document2::parser::json::parser::JsonToDocumentParser; +use serde_json::json; + +#[test] +fn test_parser_children_in_order() { + let json = json!({ + "type": "page", + "children": [ + { + "type": "paragraph1", + }, + { + "type": "paragraph2", + }, + { + "type": "paragraph3", + }, + { + "type": "paragraph4", + } + ] + }); + + let document = JsonToDocumentParser::json_str_to_document(json.to_string().as_str()).unwrap(); + + // root + 4 paragraphs + assert_eq!(document.blocks.len(), 5); + + // root + 4 paragraphs + assert_eq!(document.meta.children_map.len(), 5); + + let (page_id, page_block) = document + .blocks + .iter() + .find(|(_, block)| block.ty == "page") + .unwrap(); + + // the children should be in order + let page_children = document + .meta + .children_map + .get(page_block.children_id.as_str()) + .unwrap(); + assert_eq!(page_children.children.len(), 4); + for (i, child_id) in page_children.children.iter().enumerate() { + let child = document.blocks.get(child_id).unwrap(); + assert_eq!(child.ty, format!("paragraph{}", i + 1)); + assert_eq!(child.parent_id, page_id.to_owned()); + } +} + +#[test] +fn test_parser_nested_children() { + let json = json!({ + "type": "page", + "children": [ + { + "type": "paragraph", + "children": [ + { + "type": "paragraph", + "children": [ + { + "type": "paragraph", + "children": [ + { + "type": "paragraph" + } + ] + } + ] + } + ] + } + ] + }); + + let document = JsonToDocumentParser::json_str_to_document(json.to_string().as_str()).unwrap(); + + // root + 4 paragraphs + assert_eq!(document.blocks.len(), 5); + + // root + 4 paragraphs + assert_eq!(document.meta.children_map.len(), 5); + + let (page_id, page_block) = document + .blocks + .iter() + .find(|(_, block)| block.ty == "page") + .unwrap(); + + // first child of root is a paragraph + let page_children = document + .meta + .children_map + .get(page_block.children_id.as_str()) + .unwrap(); + assert_eq!(page_children.children.len(), 1); + let page_first_child_id = page_children.children.first().unwrap(); + let page_first_child = document.blocks.get(page_first_child_id).unwrap(); + assert_eq!(page_first_child.ty, "paragraph"); + assert_eq!(page_first_child.parent_id, page_id.to_owned()); +} diff --git a/frontend/rust-lib/flowy-document2/tests/parser/mod.rs b/frontend/rust-lib/flowy-document2/tests/parser/mod.rs new file mode 100644 index 0000000000..cff0e9089e --- /dev/null +++ b/frontend/rust-lib/flowy-document2/tests/parser/mod.rs @@ -0,0 +1 @@ +mod json; diff --git a/frontend/rust-lib/flowy-folder2/src/READ_ME.json b/frontend/rust-lib/flowy-folder2/src/READ_ME.json deleted file mode 100644 index 25d07934f9..0000000000 --- a/frontend/rust-lib/flowy-folder2/src/READ_ME.json +++ /dev/null @@ -1,181 +0,0 @@ -{ - "document": { - "type": "document", - "children": [{ - "type": "heading", - "attributes": { - "level": 2, - "delta": [{ - "insert": "👋 " - }, - { - "insert": "Welcome to", - "attributes": { - "bold": true - } - }, - { - "insert": " " - }, - { - "insert": "AppFlowy Editor", - "attributes": { - "href": "appflowy.io", - "italic": true, - "bold": true - } - } - ] - } - }, - { - "type": "paragraph", - "attributes": { - "delta": [] - } - }, - { - "type": "paragraph", - "attributes": { - "delta": [{ - "insert": "AppFlowy Editor is a" - }, - { - "insert": " " - }, - { - "insert": "highly customizable", - "attributes": { - "bold": true - } - }, - { - "insert": " " - }, - { - "insert": "rich-text editor", - "attributes": { - "italic": true - } - }, - { - "insert": " for " - }, - { - "insert": "Flutter", - "attributes": { - "underline": true - } - } - ] - } - }, - { - "type": "todo_list", - "attributes": { - "checked": true, - "delta": [{ - "insert": "Customizable" - }] - } - - }, - { - "type": "todo_list", - "attributes": { - "checked": true, - "delta": [{ - "insert": "Test-covered" - }] - } - }, - { - "type": "todo_list", - "attributes": { - "checked": false, - "delta": [{ - "insert": "more to come!" - }] - } - }, - { - "type": "paragraph", - "attributes": { - "delta": [] - } - }, - { - "type": "quote", - "attributes": { - "delta": [{ - "insert": "Here is an example you can give a try" - }] - } - }, - { - "type": "paragraph", - "attributes": { - "delta": [] - } - }, - { - "type": "paragraph", - "attributes": { - "delta": [{ - "insert": "You can also use " - }, - { - "insert": "AppFlowy Editor", - "attributes": { - "italic": true, - "bold": true, - "backgroundColor": "0x6000BCF0" - } - }, - { - "insert": " as a component to build your own app." - } - ] - } - }, - { - "type": "paragraph", - "attributes": { - "delta": [] - } - }, - { - "type": "bulleted_list", - "attributes": { - "delta": [{ - "insert": "Use / to insert blocks" - }] - } - - }, - { - "type": "bulleted_list", - "attributes": { - "delta": [{ - "insert": "Select text to trigger to the toolbar to format your notes." - }] - } - - }, - { - "type": "paragraph", - "attributes": { - "delta": [] - } - }, - { - "type": "paragraph", - "attributes": { - "delta": [{ - "insert": "If you have questions or feedback, please submit an issue on Github or join the community along with 1000+ builders!" - }] - } - } - ] - } -} \ No newline at end of file