From 0806436c19760f2b6488bcb4dd98f12a8878adca Mon Sep 17 00:00:00 2001 From: "Nathan.fooo" <86001920+appflowy@users.noreply.github.com> Date: Mon, 28 Aug 2023 23:20:56 +0800 Subject: [PATCH] chore: share database via csv (#3285) * chore: share database via csv * fix: flutter test --- .../database_view/application/share_bloc.dart | 66 ++++++++ .../database_view/tar_bar/tab_bar_view.dart | 13 ++ .../database_view/widgets/share_button.dart | 154 ++++++++++++++++++ frontend/appflowy_tauri/src-tauri/Cargo.lock | 66 +++++--- frontend/resources/translations/en.json | 1 + .../rust-lib/flowy-document2/src/manager.rs | 15 +- 6 files changed, 288 insertions(+), 27 deletions(-) create mode 100644 frontend/appflowy_flutter/lib/plugins/database_view/application/share_bloc.dart create mode 100644 frontend/appflowy_flutter/lib/plugins/database_view/widgets/share_button.dart diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/share_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/share_bloc.dart new file mode 100644 index 0000000000..5b498b9798 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/share_bloc.dart @@ -0,0 +1,66 @@ +import 'dart:io'; +import 'package:appflowy/workspace/application/settings/share/export_service.dart'; +import 'package:appflowy_backend/log.dart'; +import 'package:appflowy_backend/protobuf/flowy-document2/entities.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:dartz/dartz.dart'; +part 'share_bloc.freezed.dart'; + +class DatabaseShareBloc extends Bloc { + DatabaseShareBloc({ + required this.view, + }) : super(const DatabaseShareState.initial()) { + on(_onShareCSV); + } + + final ViewPB view; + + Future _onShareCSV( + ShareCSV event, + Emitter emit, + ) async { + emit(const DatabaseShareState.loading()); + + final result = await BackendExportService.exportDatabaseAsCSV(view.id); + result.fold( + (l) => _saveCSVToPath(l.data, event.path), + (r) => Log.error(r), + ); + + emit( + DatabaseShareState.finish( + result.fold( + (l) { + _saveCSVToPath(l.data, event.path); + return left(unit); + }, + (r) => right(r), + ), + ), + ); + } + + ExportDataPB _saveCSVToPath(String markdown, String path) { + File(path).writeAsStringSync(markdown); + return ExportDataPB() + ..data = markdown + ..exportType = ExportType.Markdown; + } +} + +@freezed +class DatabaseShareEvent with _$DatabaseShareEvent { + const factory DatabaseShareEvent.shareCSV(String path) = ShareCSV; +} + +@freezed +class DatabaseShareState with _$DatabaseShareState { + const factory DatabaseShareState.initial() = _Initial; + const factory DatabaseShareState.loading() = _Loading; + const factory DatabaseShareState.finish( + Either successOrFail, + ) = _Finish; +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/tar_bar/tab_bar_view.dart b/frontend/appflowy_flutter/lib/plugins/database_view/tar_bar/tab_bar_view.dart index cafc372d8b..a32e706bf6 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/tar_bar/tab_bar_view.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/tar_bar/tab_bar_view.dart @@ -1,4 +1,5 @@ import 'package:appflowy/plugins/database_view/application/tar_bar_bloc.dart'; +import 'package:appflowy/plugins/database_view/widgets/share_button.dart'; import 'package:appflowy/plugins/util.dart'; import 'package:appflowy/startup/plugin/plugin.dart'; import 'package:appflowy/workspace/presentation/home/home_stack.dart'; @@ -212,4 +213,16 @@ class DatabasePluginWidgetBuilder extends PluginWidgetBuilder { @override List get navigationItems => [this]; + + @override + Widget? get rightBarItem { + return Row( + children: [ + DatabaseShareButton( + key: ValueKey(notifier.view.id), + view: notifier.view, + ), + ], + ); + } } diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/share_button.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/share_button.dart new file mode 100644 index 0000000000..091bd44b22 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/share_button.dart @@ -0,0 +1,154 @@ +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/plugins/database_view/application/share_bloc.dart'; +import 'package:appflowy/startup/startup.dart'; +import 'package:appflowy/workspace/application/view/view_listener.dart'; +import 'package:appflowy/workspace/presentation/home/toast.dart'; +import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart'; +import 'package:appflowy_popover/appflowy_popover.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra/file_picker/file_picker_service.dart'; +import 'package:flowy_infra_ui/widget/rounded_button.dart'; +import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class DatabaseShareButton extends StatelessWidget { + const DatabaseShareButton({ + super.key, + required this.view, + }); + + final ViewPB view; + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => DatabaseShareBloc(view: view), + child: BlocListener( + listener: (context, state) { + state.mapOrNull( + finish: (state) { + state.successOrFail.fold( + (data) => _handleExportData(context), + _handleExportError, + ); + }, + ); + }, + child: BlocBuilder( + builder: (context, state) => ConstrainedBox( + constraints: const BoxConstraints.expand( + height: 30, + width: 100, + ), + child: DatabaseShareActionList(view: view), + ), + ), + ), + ); + } + + void _handleExportData(BuildContext context) { + showSnackBarMessage( + context, + LocaleKeys.settings_files_exportFileSuccess.tr(), + ); + } + + void _handleExportError(FlowyError error) { + showMessageToast(error.msg); + } +} + +class DatabaseShareActionList extends StatefulWidget { + const DatabaseShareActionList({ + super.key, + required this.view, + }); + + final ViewPB view; + + @override + State createState() => + DatabaseShareActionListState(); +} + +@visibleForTesting +class DatabaseShareActionListState extends State { + late String name; + late final ViewListener viewListener = ViewListener(viewId: widget.view.id); + + @override + void initState() { + super.initState(); + listenOnViewUpdated(); + } + + @override + void dispose() { + viewListener.stop(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final databaseShareBloc = context.read(); + return PopoverActionList( + direction: PopoverDirection.bottomWithCenterAligned, + offset: const Offset(0, 8), + actions: ShareAction.values + .map((action) => ShareActionWrapper(action)) + .toList(), + buildChild: (controller) { + return RoundedTextButton( + title: LocaleKeys.shareAction_buttonText.tr(), + onPressed: () => controller.show(), + ); + }, + onSelected: (action, controller) async { + switch (action.inner) { + case ShareAction.csv: + final exportPath = await getIt().saveFile( + dialogTitle: '', + fileName: '${Uri.encodeComponent(name)}.csv', + ); + if (exportPath != null) { + databaseShareBloc.add(DatabaseShareEvent.shareCSV(exportPath)); + } + break; + } + controller.close(); + }, + ); + } + + void listenOnViewUpdated() { + name = widget.view.name; + viewListener.start( + onViewUpdated: (view) { + name = view.name; + }, + ); + } +} + +enum ShareAction { + csv, +} + +class ShareActionWrapper extends ActionCell { + final ShareAction inner; + + ShareActionWrapper(this.inner); + + Widget? icon(Color iconColor) => null; + + @override + String get name { + switch (inner) { + case ShareAction.csv: + return LocaleKeys.shareAction_csv.tr(); + } + } +} diff --git a/frontend/appflowy_tauri/src-tauri/Cargo.lock b/frontend/appflowy_tauri/src-tauri/Cargo.lock index 578a148921..abe9b5f65b 100644 --- a/frontend/appflowy_tauri/src-tauri/Cargo.lock +++ b/frontend/appflowy_tauri/src-tauri/Cargo.lock @@ -140,7 +140,7 @@ checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" [[package]] name = "appflowy-integrate" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=cff1b9#cff1b99f4ed51f65dab73492eac4da8e7907f079" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=1b297c#1b297c2ed75aa33b964f0da546d771b00805be62" dependencies = [ "anyhow", "collab", @@ -728,7 +728,7 @@ dependencies = [ [[package]] name = "collab" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=cff1b9#cff1b99f4ed51f65dab73492eac4da8e7907f079" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=1b297c#1b297c2ed75aa33b964f0da546d771b00805be62" dependencies = [ "anyhow", "bytes", @@ -746,7 +746,7 @@ dependencies = [ [[package]] name = "collab-client-ws" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=cff1b9#cff1b99f4ed51f65dab73492eac4da8e7907f079" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=1b297c#1b297c2ed75aa33b964f0da546d771b00805be62" dependencies = [ "bytes", "collab-sync", @@ -764,7 +764,7 @@ dependencies = [ [[package]] name = "collab-database" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=cff1b9#cff1b99f4ed51f65dab73492eac4da8e7907f079" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=1b297c#1b297c2ed75aa33b964f0da546d771b00805be62" dependencies = [ "anyhow", "async-trait", @@ -781,6 +781,8 @@ dependencies = [ "serde", "serde_json", "serde_repr", + "strum", + "strum_macros 0.25.2", "thiserror", "tokio", "tokio-stream", @@ -791,7 +793,7 @@ dependencies = [ [[package]] name = "collab-define" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=cff1b9#cff1b99f4ed51f65dab73492eac4da8e7907f079" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=1b297c#1b297c2ed75aa33b964f0da546d771b00805be62" dependencies = [ "uuid", ] @@ -799,7 +801,7 @@ dependencies = [ [[package]] name = "collab-derive" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=cff1b9#cff1b99f4ed51f65dab73492eac4da8e7907f079" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=1b297c#1b297c2ed75aa33b964f0da546d771b00805be62" dependencies = [ "proc-macro2", "quote", @@ -811,7 +813,7 @@ dependencies = [ [[package]] name = "collab-document" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=cff1b9#cff1b99f4ed51f65dab73492eac4da8e7907f079" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=1b297c#1b297c2ed75aa33b964f0da546d771b00805be62" dependencies = [ "anyhow", "collab", @@ -830,7 +832,7 @@ dependencies = [ [[package]] name = "collab-folder" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=cff1b9#cff1b99f4ed51f65dab73492eac4da8e7907f079" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=1b297c#1b297c2ed75aa33b964f0da546d771b00805be62" dependencies = [ "anyhow", "chrono", @@ -850,7 +852,7 @@ dependencies = [ [[package]] name = "collab-persistence" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=cff1b9#cff1b99f4ed51f65dab73492eac4da8e7907f079" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=1b297c#1b297c2ed75aa33b964f0da546d771b00805be62" dependencies = [ "bincode", "chrono", @@ -870,7 +872,7 @@ dependencies = [ [[package]] name = "collab-plugins" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=cff1b9#cff1b99f4ed51f65dab73492eac4da8e7907f079" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=1b297c#1b297c2ed75aa33b964f0da546d771b00805be62" dependencies = [ "anyhow", "async-trait", @@ -899,7 +901,7 @@ dependencies = [ [[package]] name = "collab-sync" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=cff1b9#cff1b99f4ed51f65dab73492eac4da8e7907f079" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=1b297c#1b297c2ed75aa33b964f0da546d771b00805be62" dependencies = [ "bytes", "collab", @@ -921,7 +923,7 @@ dependencies = [ [[package]] name = "collab-user" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=cff1b9#cff1b99f4ed51f65dab73492eac4da8e7907f079" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=1b297c#1b297c2ed75aa33b964f0da546d771b00805be62" dependencies = [ "anyhow", "collab", @@ -1544,7 +1546,7 @@ dependencies = [ "flowy-sqlite", "lib-dispatch", "protobuf", - "strum_macros", + "strum_macros 0.21.1", ] [[package]] @@ -1629,7 +1631,7 @@ dependencies = [ "serde_json", "serde_repr", "strum", - "strum_macros", + "strum_macros 0.25.2", "tokio", "tracing", "url", @@ -1683,7 +1685,7 @@ dependencies = [ "protobuf", "serde", "serde_json", - "strum_macros", + "strum_macros 0.21.1", "tokio", "tokio-stream", "tracing", @@ -1756,7 +1758,7 @@ dependencies = [ "nanoid", "parking_lot 0.12.1", "protobuf", - "strum_macros", + "strum_macros 0.21.1", "tokio", "tokio-stream", "tracing", @@ -1868,11 +1870,13 @@ dependencies = [ name = "flowy-user" version = "0.1.0" dependencies = [ + "anyhow", "appflowy-integrate", "base64 0.21.2", "bytes", "chrono", "collab", + "collab-database", "collab-document", "collab-folder", "collab-user", @@ -1883,6 +1887,7 @@ dependencies = [ "flowy-derive", "flowy-encrypt", "flowy-error", + "flowy-folder-deps", "flowy-notification", "flowy-server-config", "flowy-sqlite", @@ -1897,7 +1902,7 @@ dependencies = [ "serde", "serde_json", "serde_repr", - "strum_macros", + "strum_macros 0.21.1", "tokio", "tracing", "unicode-segmentation", @@ -3464,6 +3469,15 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" +[[package]] +name = "openssl-src" +version = "111.27.0+1.1.1v" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06e8f197c82d7511c5b014030c9b1efeda40d7d5f99d23b4ceed3524a5e63f02" +dependencies = [ + "cc", +] + [[package]] name = "openssl-sys" version = "0.9.90" @@ -3472,6 +3486,7 @@ checksum = "374533b0e45f3a7ced10fcaeccca020e66656bc03dac384f852e4e5a7a8104a6" dependencies = [ "cc", "libc", + "openssl-src", "pkg-config", "vcpkg", ] @@ -5041,9 +5056,9 @@ checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" [[package]] name = "strum" -version = "0.21.0" +version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aaf86bbcfd1fa9670b7a129f64fc0c9fcbbfe4f1bc4210e9e98fe71ffc12cde2" +checksum = "290d54ea6f91c969195bdbcd7442c8c2a2ba87da8bf60a7ee86a235d4bc1e125" [[package]] name = "strum_macros" @@ -5057,6 +5072,19 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "strum_macros" +version = "0.25.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad8d03b598d3d0fff69bf533ee3ef19b8eeb342729596df84bcc7e1f96ec4059" +dependencies = [ + "heck 0.4.1", + "proc-macro2", + "quote", + "rustversion", + "syn 2.0.22", +] + [[package]] name = "subtle" version = "2.5.0" diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index d7df1f1970..6a93a3e835 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -55,6 +55,7 @@ "buttonText": "Share", "workInProgress": "Coming soon", "markdown": "Markdown", + "csv": "CSV", "copyLink": "Copy Link" }, "moreAction": { diff --git a/frontend/rust-lib/flowy-document2/src/manager.rs b/frontend/rust-lib/flowy-document2/src/manager.rs index 6d4e2e784e..66e8479957 100644 --- a/frontend/rust-lib/flowy-document2/src/manager.rs +++ b/frontend/rust-lib/flowy-document2/src/manager.rs @@ -64,15 +64,14 @@ impl DocumentManager { data: Option, ) -> FlowyResult> { tracing::trace!("create a document: {:?}", doc_id); - let collab = self.collab_for_document(uid, doc_id, vec![])?; - match self.get_document(doc_id).await { - Ok(document) => Ok(document), - Err(_) => { - let data = data.unwrap_or_else(default_document_data); - let document = Arc::new(MutexDocument::create_with_data(collab, data)?); - Ok(document) - }, + if self.is_doc_exist(doc_id).unwrap_or(false) { + self.get_document(doc_id).await + } else { + let collab = self.collab_for_document(uid, doc_id, vec![])?; + let data = data.unwrap_or_else(default_document_data); + let document = Arc::new(MutexDocument::create_with_data(collab, data)?); + Ok(document) } }