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