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 <nathan@appflowy.io>

* 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 <nathan@appflowy.io>
This commit is contained in:
Lucas.Xu 2023-06-01 20:18:40 +08:00 committed by GitHub
parent 012b6c0066
commit 2ef72f3203
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
41 changed files with 1043 additions and 298 deletions

View File

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

View File

@ -1 +1,4 @@
<svg xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" class="plus" style="width: 16px; height: 100%; display: block; fill: inherit; flex-shrink: 0; backface-visibility: hidden;" width="16" height="16" ><path d="M7.977 14.963c.407 0 .747-.324.747-.723V8.72h5.362c.399 0 .74-.34.74-.747a.746.746 0 00-.74-.738H8.724V1.706c0-.398-.34-.722-.747-.722a.732.732 0 00-.739.722v5.529h-5.37a.746.746 0 00-.74.738c0 .407.341.747.74.747h5.37v5.52c0 .399.332.723.739.723z" fill-opacity="0.35" fill="#37352F"></path></svg>
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="7.5" y="4" width="1" height="8" rx="0.5" fill="#333333"/>
<rect x="12" y="7.5" width="1" height="8" rx="0.5" transform="rotate(90 12 7.5)" fill="#333333"/>
</svg>

Before

Width:  |  Height:  |  Size: 558 B

After

Width:  |  Height:  |  Size: 268 B

View File

@ -0,0 +1,29 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
width="512.000000pt" height="512.000000pt" viewBox="0 0 512.000000 512.000000"
preserveAspectRatio="xMidYMid meet">
<g transform="translate(0.000000,512.000000) scale(0.100000,-0.100000)"
fill="#000000" stroke="none">
<path d="M2990 5100 c-19 -10 -382 -366 -806 -792 l-771 -773 -42 -85 c-125
-256 -84 -501 120 -714 47 -50 91 -101 98 -113 14 -27 14 -116 0 -168 -57
-207 -310 -495 -535 -606 -73 -37 -92 -42 -190 -50 -60 -4 -145 -18 -189 -29
-312 -80 -560 -328 -646 -645 -32 -116 -32 -336 -1 -450 88 -318 330 -560 647
-646 115 -32 335 -32 450 0 263 71 474 247 591 494 45 95 71 203 83 337 7 90
15 122 40 176 149 318 583 627 777 554 15 -6 68 -50 119 -98 214 -205 459
-246 715 -121 85 42 85 42 849 803 420 418 776 781 792 805 34 53 37 97 10
149 -24 46 -1934 1954 -1978 1976 -42 21 -90 20 -133 -4z m920 -1195 l845
-845 -318 -317 -317 -318 -847 847 -848 848 315 315 c173 173 317 315 320 315
3 0 385 -380 850 -845z m-282 -1972 c-320 -320 -330 -327 -458 -328 -100 0
-145 23 -264 135 -52 49 -114 99 -137 111 -242 123 -589 11 -902 -291 -135
-130 -224 -245 -287 -375 -54 -110 -80 -200 -80 -285 -2 -277 -206 -528 -475
-585 -203 -43 -395 15 -545 164 -190 188 -233 466 -108 704 42 81 164 203 245
245 77 41 198 72 277 72 326 1 780 374 949 780 74 178 76 355 5 494 -11 21
-59 80 -108 132 -110 117 -135 164 -135 259 0 131 3 136 324 458 l286 287 847
-847 848 -848 -282 -282z"/>
<path d="M828 1029 c-43 -22 -78 -81 -78 -129 0 -76 74 -150 150 -150 17 0 49
9 70 20 45 23 80 80 80 130 0 50 -35 107 -80 130 -49 25 -94 25 -142 -1z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@ -1,6 +1,6 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3 4.3999H4.11111H13" stroke="#333333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M5.77775 4.4V3.2C5.77775 2.88174 5.89481 2.57652 6.10319 2.35147C6.31156 2.12643 6.59418 2 6.88886 2H9.11108C9.40577 2 9.68838 2.12643 9.89676 2.35147C10.1051 2.57652 10.2222 2.88174 10.2222 3.2V4.4M11.8889 4.4V12.8C11.8889 13.1183 11.7718 13.4235 11.5634 13.6485C11.3551 13.8736 11.0724 14 10.7778 14H5.2222C4.92751 14 4.64489 13.8736 4.43652 13.6485C4.22815 13.4235 4.11108 13.1183 4.11108 12.8V4.4H11.8889Z" stroke="#333333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M6.88892 7.3999V10.9999" stroke="#333333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M9.11108 7.3999V10.9999" stroke="#333333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M3 4.40039H4.11111H13" stroke="#BBC3CD" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M5.77799 4.4V3.2C5.77799 2.88174 5.89506 2.57652 6.10343 2.35147C6.31181 2.12643 6.59442 2 6.88911 2H9.11133C9.40601 2 9.68863 2.12643 9.897 2.35147C10.1054 2.57652 10.2224 2.88174 10.2224 3.2V4.4M11.8891 4.4V12.8C11.8891 13.1183 11.772 13.4235 11.5637 13.6485C11.3553 13.8736 11.0727 14 10.778 14H5.22244C4.92775 14 4.64514 13.8736 4.43676 13.6485C4.22839 13.4235 4.11133 13.1183 4.11133 12.8V4.4H11.8891Z" stroke="#BBC3CD" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M6.88867 7.40039V11.0004" stroke="#BBC3CD" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M9.11133 7.40039V11.0004" stroke="#BBC3CD" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 886 B

After

Width:  |  Height:  |  Size: 886 B

View File

@ -1,4 +1,4 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M11.974 6.33301H7.35865C6.7922 6.33301 6.33301 6.7922 6.33301 7.35865V11.974C6.33301 12.5405 6.7922 12.9997 7.35865 12.9997H11.974C12.5405 12.9997 12.9997 12.5405 12.9997 11.974V7.35865C12.9997 6.7922 12.5405 6.33301 11.974 6.33301Z" stroke="#333333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M4.53846 9.66667H4.02564C3.75362 9.66667 3.49275 9.55861 3.3004 9.36626C3.10806 9.17392 3 8.91304 3 8.64103V4.02564C3 3.75362 3.10806 3.49275 3.3004 3.3004C3.49275 3.10806 3.75362 3 4.02564 3H8.64103C8.91304 3 9.17392 3.10806 9.36626 3.3004C9.55861 3.49275 9.66667 3.75362 9.66667 4.02564V4.53846" stroke="#333333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M11.974 6.33301H7.35865C6.7922 6.33301 6.33301 6.7922 6.33301 7.35865V11.974C6.33301 12.5405 6.7922 12.9997 7.35865 12.9997H11.974C12.5405 12.9997 12.9997 12.5405 12.9997 11.974V7.35865C12.9997 6.7922 12.5405 6.33301 11.974 6.33301Z" stroke="#BBC3CD" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M4.53846 9.66667H4.02564C3.75362 9.66667 3.49275 9.55861 3.3004 9.36626C3.10806 9.17392 3 8.91304 3 8.64103V4.02564C3 3.75362 3.10806 3.49275 3.3004 3.3004C3.49275 3.10806 3.75362 3 4.02564 3H8.64103C8.91304 3 9.17392 3.10806 9.36626 3.3004C9.55861 3.49275 9.66667 3.75362 9.66667 4.02564V4.53846" stroke="#BBC3CD" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 785 B

After

Width:  |  Height:  |  Size: 785 B

View File

@ -1 +1,8 @@
<svg xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 10 10" class="dragHandle" style="width: 14px; height: 14px; display: block; fill: inherit; flex-shrink: 0; backface-visibility: hidden;" width="14" height="14" ><path d="M3,2 C2.44771525,2 2,1.55228475 2,1 C2,0.44771525 2.44771525,0 3,0 C3.55228475,0 4,0.44771525 4,1 C4,1.55228475 3.55228475,2 3,2 Z M3,6 C2.44771525,6 2,5.55228475 2,5 C2,4.44771525 2.44771525,4 3,4 C3.55228475,4 4,4.44771525 4,5 C4,5.55228475 3.55228475,6 3,6 Z M3,10 C2.44771525,10 2,9.55228475 2,9 C2,8.44771525 2.44771525,8 3,8 C3.55228475,8 4,8.44771525 4,9 C4,9.55228475 3.55228475,10 3,10 Z M7,2 C6.44771525,2 6,1.55228475 6,1 C6,0.44771525 6.44771525,0 7,0 C7.55228475,0 8,0.44771525 8,1 C8,1.55228475 7.55228475,2 7,2 Z M7,6 C6.44771525,6 6,5.55228475 6,5 C6,4.44771525 6.44771525,4 7,4 C7.55228475,4 8,4.44771525 8,5 C8,5.55228475 7.55228475,6 7,6 Z M7,10 C6.44771525,10 6,9.55228475 6,9 C6,8.44771525 6.44771525,8 7,8 C7.55228475,8 8,8.44771525 8,9 C8,9.55228475 7.55228475,10 7,10 Z" fill-opacity="0.35" fill="#37352F"></path></svg>
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="9" y="3" width="2" height="2" rx="0.5" fill="#333333"/>
<rect x="5" y="3" width="2" height="2" rx="0.5" fill="#333333"/>
<rect x="9" y="7" width="2" height="2" rx="0.5" fill="#333333"/>
<rect x="5" y="7" width="2" height="2" rx="0.5" fill="#333333"/>
<rect x="9" y="11" width="2" height="2" rx="0.5" fill="#333333"/>
<rect x="5" y="11" width="2" height="2" rx="0.5" fill="#333333"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 495 B

View File

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

View File

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

View File

@ -114,6 +114,7 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
maxWidth: double.infinity,
),
child: FloatingToolbar(
style: styleCustomizer.floatingToolbarStyleBuilder(),
items: toolbarItems,
editorState: widget.editorState,
scrollController: scrollController,
@ -127,16 +128,16 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
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<AppFlowyEditorPage> {
// 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<AppFlowyEditorPage> {
];
final colorAction = [
OptionAction.divider,
// OptionAction.divider,
OptionAction.color,
];

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -136,4 +136,11 @@ class EditorStyleCustomizer {
selectionMenuItemSelectedColor: theme.hoverColor,
);
}
FloatingToolbarStyle floatingToolbarStyleBuilder() {
final theme = Theme.of(context);
return FloatingToolbarStyle(
backgroundColor: theme.cardColor,
);
}
}

View File

@ -8,6 +8,20 @@ class SettingsFileExportState {
initialize();
}
List<ViewPB> get selectedViews {
final selectedViews = <ViewPB>[];
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<ViewPB> views;
List<bool> expanded = [];
List<bool> selectedApps = [];

View File

@ -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<SettingsExportFileWidget> createState() =>
SettingsExportFileWidgetState();
}
@visibleForTesting
class SettingsExportFileWidgetState extends State<SettingsExportFileWidget> {
@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,
);
}
}

View File

@ -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<FileExporterWidget> {
// Map<String, List<String>> _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<dartz.Either<WorkspaceSettingPB, FlowyError>>(
future: FolderEventReadCurrentWorkspace().send(),
builder: (context, snapshot) {
if (snapshot.hasData &&
snapshot.connectionState == ConnectionState.done) {
final workspaces = snapshot.data?.getLeftOrNull<WorkspaceSettingPB>();
if (workspaces != null) {
final views = workspaces.workspace.views;
cubit ??= SettingsFileExporterCubit(views: views);
return BlocProvider<SettingsFileExporterCubit>.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<FileExporterWidget> {
onPressed: () async {
await getIt<FilePickerService>()
.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<FileExporterWidget> {
);
}
FutureBuilder<dartz.Either<WorkspaceSettingPB, FlowyError>>
_buildFileSelector(BuildContext context) {
return FutureBuilder<dartz.Either<WorkspaceSettingPB, FlowyError>>(
future: FolderEventReadCurrentWorkspace().send(),
builder: (context, snapshot) {
if (snapshot.hasData &&
snapshot.connectionState == ConnectionState.done) {
final workspaces = snapshot.data?.getLeftOrNull<WorkspaceSettingPB>();
if (workspaces != null) {
final views = workspaces.workspace.views;
return BlocProvider<SettingsFileExporterCubit>(
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<Widget> 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<String> failedNames)> exportToPath(
String path,
List<ViewPB> views,
) async {
final failedFileNames = <String>[];
final Map<String, int> 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);
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1692,6 +1692,7 @@ dependencies = [
"flowy-derive",
"flowy-error",
"flowy-notification",
"indexmap",
"lib-dispatch",
"nanoid",
"parking_lot 0.12.1",

View File

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

View File

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

View File

@ -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<String, Value>,
pub is_primary: bool,
}
#[derive(Debug, Clone, Serialize)]
struct ExportCell {
data: String,
field_type: FieldType,
}
impl From<&Arc<FieldRevision>> for ExportField {
fn from(field_rev: &Arc<FieldRevision>) -> Self {
let field_type = FieldType::from(field_rev.ty);
let mut type_options: HashMap<String, Value> = 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<DatabaseEditor>,
) -> FlowyResult<String> {
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::<Vec<String>>();
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::<DateCellData>(&data.cell_str) {
Ok(cell_data) => cell_data.timestamp.unwrap_or_default().to_string(),
Err(_) => "".to_string(),
}
},
FieldType::URL => match serde_json::from_str::<URLCellData>(&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::<Vec<_>>();
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)
}
}

View File

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

View File

@ -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<String, ChildrenPB>,
}
#[derive(Default, ProtoBuf)]
#[derive(Default, ProtoBuf, Debug)]
pub struct ChildrenPB {
#[pb(index = 1)]
pub children: Vec<String>,

View File

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

View File

@ -0,0 +1,20 @@
use std::collections::HashMap;
use serde::{Deserialize, Serialize};
use serde_json::Value;
/// Json format:
/// {
/// 'type': string,
/// 'data': Map<String, Object>
/// 'children': [Block],
/// }
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Block {
#[serde(rename = "type")]
pub ty: String,
#[serde(default)]
pub data: HashMap<String, Value>,
#[serde(default)]
pub children: Vec<Block>,
}

View File

@ -0,0 +1,2 @@
pub mod block;
pub mod parser;

View File

@ -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<DocumentDataPB> {
let root = serde_json::from_str::<Block>(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<String>,
parent_id: String,
) -> IndexMap<String, BlockPB> {
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<String, BlockPB>) -> HashMap<String, ChildrenPB> {
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<String>, 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),
}
}
}

View File

@ -0,0 +1 @@
pub mod json;

View File

@ -1 +1,2 @@
mod document;
mod parser;

View File

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

View File

@ -0,0 +1,2 @@
mod block_test;
mod parser_test;

View File

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

View File

@ -0,0 +1 @@
mod json;

View File

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