chore: implement import csv ui (#2710)

* chore: implement import csv ui

* feat: support importing CSV

---------

Co-authored-by: Lucas.Xu <lucas.xu@appflowy.io>
This commit is contained in:
Nathan.fooo 2023-06-05 18:29:52 +08:00 committed by GitHub
parent 5b59800449
commit e24a8aabeb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 77 additions and 42 deletions

View file

@ -1,6 +1,3 @@
import 'dart:convert';
import 'dart:typed_data';
import 'package:appflowy_backend/dispatch/dispatch.dart'; import 'package:appflowy_backend/dispatch/dispatch.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-folder2/import.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-folder2/import.pb.dart';
@ -8,33 +5,31 @@ import 'package:appflowy_backend/protobuf/flowy-folder2/view.pbenum.dart';
import 'package:dartz/dartz.dart'; import 'package:dartz/dartz.dart';
class ImportBackendService { class ImportBackendService {
static Future<Either<Unit, FlowyError>> importHistoryDatabase( static Future<Either<Unit, FlowyError>> importData(
String data, List<int> data,
String name,
String parentViewId,
) async {
final payload = ImportPB.create()
..data = utf8.encode(data)
..parentViewId = parentViewId
..viewLayout = ViewLayoutPB.Grid
..name = name
..importType = ImportTypePB.HistoryDatabase;
return await FolderEventImportData(payload).send();
}
static Future<Either<Unit, FlowyError>> importHistoryDocument(
Uint8List data,
String name, String name,
String parentViewId, String parentViewId,
ImportTypePB importType,
) async { ) async {
final payload = ImportPB.create() final payload = ImportPB.create()
..data = data ..data = data
..parentViewId = parentViewId ..parentViewId = parentViewId
..viewLayout = ViewLayoutPB.Document ..viewLayout = importType.toLayout()
..name = name ..name = name
..importType = ImportTypePB.HistoryDocument; ..importType = importType;
return await FolderEventImportData(payload).send(); return await FolderEventImportData(payload).send();
} }
} }
extension on ImportTypePB {
ViewLayoutPB toLayout() {
switch (this) {
case ImportTypePB.HistoryDocument:
return ViewLayoutPB.Document;
case ImportTypePB.HistoryDatabase || ImportTypePB.CSV:
return ViewLayoutPB.Grid;
default:
throw UnimplementedError('Unsupported import type $this');
}
}
}

View file

@ -87,6 +87,7 @@ class AddButton extends StatelessWidget {
switch (type) { switch (type) {
case ImportType.historyDocument: case ImportType.historyDocument:
case ImportType.historyDatabase: case ImportType.historyDatabase:
case ImportType.databaseCSV:
onSelected( onSelected(
action.pluginBuilder, action.pluginBuilder,
name, name,

View file

@ -1,3 +1,4 @@
import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'dart:typed_data'; import 'dart:typed_data';
@ -6,6 +7,7 @@ import 'package:appflowy/plugins/document/presentation/editor_plugins/migration/
import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/util/file_picker/file_picker_service.dart'; import 'package:appflowy/util/file_picker/file_picker_service.dart';
import 'package:appflowy/workspace/application/settings/share/import_service.dart'; import 'package:appflowy/workspace/application/settings/share/import_service.dart';
import 'package:appflowy_backend/protobuf/flowy-folder2/protobuf.dart';
import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:file_picker/file_picker.dart'; import 'package:file_picker/file_picker.dart';
import 'package:flowy_infra/image.dart'; import 'package:flowy_infra/image.dart';
@ -52,7 +54,8 @@ Future<void> showImportPanel(
enum ImportType { enum ImportType {
historyDocument, historyDocument,
historyDatabase, historyDatabase,
markdownOrText; markdownOrText,
databaseCSV;
@override @override
String toString() { String toString() {
@ -63,6 +66,8 @@ enum ImportType {
return 'Database from v0.1'; return 'Database from v0.1';
case ImportType.markdownOrText: case ImportType.markdownOrText:
return 'Text & Markdown'; return 'Text & Markdown';
case ImportType.databaseCSV:
return 'CSV';
default: default:
assert(false, 'Unsupported Type $this'); assert(false, 'Unsupported Type $this');
return ''; return '';
@ -70,22 +75,24 @@ enum ImportType {
} }
Widget? Function(BuildContext context) get icon => (context) { Widget? Function(BuildContext context) get icon => (context) {
var name = '';
switch (this) { switch (this) {
case ImportType.historyDocument: case ImportType.historyDocument:
name = 'editor/board';
case ImportType.historyDatabase: case ImportType.historyDatabase:
return svgWidget( name = 'editor/documents';
'editor/documents', case ImportType.databaseCSV:
color: Theme.of(context).iconTheme.color, name = 'editor/board';
);
case ImportType.markdownOrText: case ImportType.markdownOrText:
return svgWidget( name = 'editor/text';
'editor/documents',
color: Theme.of(context).iconTheme.color,
);
default: default:
assert(false, 'Unsupported Type $this'); assert(false, 'Unsupported Type $this');
return null; return null;
} }
return svgWidget(
name,
color: Theme.of(context).iconTheme.color,
);
}; };
List<String> get allowedExtensions { List<String> get allowedExtensions {
@ -96,6 +103,8 @@ enum ImportType {
return ['afdb']; return ['afdb'];
case ImportType.markdownOrText: case ImportType.markdownOrText:
return ['md', 'txt']; return ['md', 'txt'];
case ImportType.databaseCSV:
return ['csv'];
default: default:
assert(false, 'Unsupported Type $this'); assert(false, 'Unsupported Type $this');
return []; return [];
@ -105,6 +114,7 @@ enum ImportType {
bool get allowMultiSelect { bool get allowMultiSelect {
switch (this) { switch (this) {
case ImportType.historyDocument: case ImportType.historyDocument:
case ImportType.databaseCSV:
return true; return true;
case ImportType.historyDatabase: case ImportType.historyDatabase:
case ImportType.markdownOrText: case ImportType.markdownOrText:
@ -189,18 +199,28 @@ class _ImportPanelState extends State<_ImportPanel> {
case ImportType.historyDocument: case ImportType.historyDocument:
final bytes = _documentDataFrom(importType, data); final bytes = _documentDataFrom(importType, data);
if (bytes != null) { if (bytes != null) {
await ImportBackendService.importHistoryDocument( await ImportBackendService.importData(
bytes, bytes,
name, name,
parentViewId, parentViewId,
ImportTypePB.HistoryDocument,
); );
} }
break; break;
case ImportType.historyDatabase: case ImportType.historyDatabase:
await ImportBackendService.importHistoryDatabase( await ImportBackendService.importData(
data, utf8.encode(data),
name, name,
parentViewId, parentViewId,
ImportTypePB.HistoryDatabase,
);
break;
case ImportType.databaseCSV:
await ImportBackendService.importData(
utf8.encode(data),
name,
parentViewId,
ImportTypePB.CSV,
); );
break; break;
default: default:

View file

@ -18,6 +18,7 @@ use flowy_error::FlowyError;
use flowy_folder2::deps::{FolderCloudService, FolderUser}; use flowy_folder2::deps::{FolderCloudService, FolderUser};
use flowy_folder2::entities::ViewLayoutPB; use flowy_folder2::entities::ViewLayoutPB;
use flowy_folder2::manager::Folder2Manager; use flowy_folder2::manager::Folder2Manager;
use flowy_folder2::share::ImportType;
use flowy_folder2::view_operation::{ use flowy_folder2::view_operation::{
FolderOperationHandler, FolderOperationHandlers, View, WorkspaceViewBuilder, FolderOperationHandler, FolderOperationHandlers, View, WorkspaceViewBuilder,
}; };
@ -189,6 +190,7 @@ impl FolderOperationHandler for DocumentFolderOperation {
&self, &self,
view_id: &str, view_id: &str,
_name: &str, _name: &str,
_import_type: ImportType,
bytes: Vec<u8>, bytes: Vec<u8>,
) -> FutureResult<(), FlowyError> { ) -> FutureResult<(), FlowyError> {
let view_id = view_id.to_string(); let view_id = view_id.to_string();
@ -315,14 +317,20 @@ impl FolderOperationHandler for DatabaseFolderOperation {
&self, &self,
view_id: &str, view_id: &str,
_name: &str, _name: &str,
import_type: ImportType,
bytes: Vec<u8>, bytes: Vec<u8>,
) -> FutureResult<(), FlowyError> { ) -> FutureResult<(), FlowyError> {
let database_manager = self.0.clone(); let database_manager = self.0.clone();
let view_id = view_id.to_string(); let view_id = view_id.to_string();
let format = match import_type {
ImportType::CSV => CSVFormat::Original,
ImportType::HistoryDatabase => CSVFormat::META,
_ => CSVFormat::Original,
};
FutureResult::new(async move { FutureResult::new(async move {
let content = String::from_utf8(bytes).map_err(|err| FlowyError::internal().context(err))?; let content = String::from_utf8(bytes).map_err(|err| FlowyError::internal().context(err))?;
database_manager database_manager
.import_csv(view_id, content, CSVFormat::META) .import_csv(view_id, content, format)
.await?; .await?;
Ok(()) Ok(())
}) })

View file

@ -85,10 +85,7 @@ fn database_from_fields_and_rows(
CSVFormat::META => { CSVFormat::META => {
// //
match serde_json::from_str(&field_meta) { match serde_json::from_str(&field_meta) {
Ok(field) => { Ok(field) => field,
//
field
},
Err(e) => { Err(e) => {
dbg!(e); dbg!(e);
default_field(field_meta, index == 0) default_field(field_meta, index == 0)
@ -197,4 +194,13 @@ mod tests {
println!("{:?}", result); println!("{:?}", result);
} }
#[test]
fn import_empty_csv_data_test() {
let s = r#""#;
let importer = CSVImporter;
let result =
importer.import_csv_from_string(gen_database_view_id(), s.to_string(), CSVFormat::Original);
assert!(result.is_err());
}
} }

View file

@ -9,6 +9,7 @@ use flowy_error::FlowyError;
pub enum ImportTypePB { pub enum ImportTypePB {
HistoryDocument = 0, HistoryDocument = 0,
HistoryDatabase = 1, HistoryDatabase = 1,
CSV = 2,
} }
impl From<ImportTypePB> for ImportType { impl From<ImportTypePB> for ImportType {
@ -16,6 +17,7 @@ impl From<ImportTypePB> for ImportType {
match pb { match pb {
ImportTypePB::HistoryDocument => ImportType::HistoryDocument, ImportTypePB::HistoryDocument => ImportType::HistoryDocument,
ImportTypePB::HistoryDatabase => ImportType::HistoryDatabase, ImportTypePB::HistoryDatabase => ImportType::HistoryDatabase,
ImportTypePB::CSV => ImportType::CSV,
} }
} }
} }

View file

@ -8,7 +8,7 @@ mod user_default;
pub mod view_operation; pub mod view_operation;
pub mod deps; pub mod deps;
mod share; pub mod share;
#[cfg(feature = "test_helper")] #[cfg(feature = "test_helper")]
mod test_helper; mod test_helper;

View file

@ -497,7 +497,7 @@ impl Folder2Manager {
let view_id = gen_view_id(); let view_id = gen_view_id();
if let Some(data) = import_data.data { if let Some(data) = import_data.data {
handler handler
.import_from_bytes(&view_id, &import_data.name, data) .import_from_bytes(&view_id, &import_data.name, import_data.import_type, data)
.await?; .await?;
} }

View file

@ -4,6 +4,7 @@ use collab_folder::core::ViewLayout;
pub enum ImportType { pub enum ImportType {
HistoryDocument = 0, HistoryDocument = 0,
HistoryDatabase = 1, HistoryDatabase = 1,
CSV = 2,
} }
#[derive(Clone, Debug)] #[derive(Clone, Debug)]

View file

@ -12,6 +12,7 @@ use lib_infra::future::FutureResult;
use lib_infra::util::timestamp; use lib_infra::util::timestamp;
use crate::entities::{CreateViewParams, ViewLayoutPB}; use crate::entities::{CreateViewParams, ViewLayoutPB};
use crate::share::ImportType;
pub type ViewData = Bytes; pub type ViewData = Bytes;
@ -204,6 +205,7 @@ pub trait FolderOperationHandler {
&self, &self,
view_id: &str, view_id: &str,
name: &str, name: &str,
import_type: ImportType,
bytes: Vec<u8>, bytes: Vec<u8>,
) -> FutureResult<(), FlowyError>; ) -> FutureResult<(), FlowyError>;