فهرست منبع

chore: share database via csv (#3285)

* chore: share database via csv

* fix: flutter test
Nathan.fooo 1 سال پیش
والد
کامیت
0806436c19

+ 66 - 0
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<DatabaseShareEvent, DatabaseShareState> {
+  DatabaseShareBloc({
+    required this.view,
+  }) : super(const DatabaseShareState.initial()) {
+    on<ShareCSV>(_onShareCSV);
+  }
+
+  final ViewPB view;
+
+  Future<void> _onShareCSV(
+    ShareCSV event,
+    Emitter<DatabaseShareState> 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<Unit, FlowyError> successOrFail,
+  ) = _Finish;
+}

+ 13 - 0
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<NavigationItem> get navigationItems => [this];
+
+  @override
+  Widget? get rightBarItem {
+    return Row(
+      children: [
+        DatabaseShareButton(
+          key: ValueKey(notifier.view.id),
+          view: notifier.view,
+        ),
+      ],
+    );
+  }
 }

+ 154 - 0
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<DatabaseShareBloc, DatabaseShareState>(
+        listener: (context, state) {
+          state.mapOrNull(
+            finish: (state) {
+              state.successOrFail.fold(
+                (data) => _handleExportData(context),
+                _handleExportError,
+              );
+            },
+          );
+        },
+        child: BlocBuilder<DatabaseShareBloc, DatabaseShareState>(
+          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<DatabaseShareActionList> createState() =>
+      DatabaseShareActionListState();
+}
+
+@visibleForTesting
+class DatabaseShareActionListState extends State<DatabaseShareActionList> {
+  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<DatabaseShareBloc>();
+    return PopoverActionList<ShareActionWrapper>(
+      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<FilePickerService>().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();
+    }
+  }
+}

+ 47 - 19
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"

+ 1 - 0
frontend/resources/translations/en.json

@@ -55,6 +55,7 @@
     "buttonText": "Share",
     "workInProgress": "Coming soon",
     "markdown": "Markdown",
+    "csv": "CSV",
     "copyLink": "Copy Link"
   },
   "moreAction": {

+ 8 - 9
frontend/rust-lib/flowy-document2/src/manager.rs

@@ -64,15 +64,14 @@ impl DocumentManager {
     data: Option<DocumentData>,
   ) -> FlowyResult<Arc<MutexDocument>> {
     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)
     }
   }