feat: import cvs from file (#2667)

* refactor: import file

* chore: fix tarui build
This commit is contained in:
Nathan.fooo 2023-05-31 14:08:54 +08:00 committed by GitHub
parent 2247fa8edb
commit 188b36cae6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
40 changed files with 569 additions and 633 deletions

View File

@ -37,7 +37,7 @@ extension InsertDatabase on EditorState {
final prefix = _referencedDatabasePrefix(viewPB.layout);
final ref = await AppBackendService().createView(
appId: appPB.id,
parentViewId: appPB.id,
name: "$prefix ${viewPB.name}",
layoutType: viewPB.layout,
ext: {

View File

@ -25,7 +25,7 @@ SelectionMenuItem boardViewMenuItem(DocumentBloc documentBloc) =>
final service = AppBackendService();
final result = (await service.createView(
appId: appId,
parentViewId: appId,
name: LocaleKeys.menuAppHeader_defaultNewPageName.tr(),
layoutType: ViewLayoutPB.Board,
))

View File

@ -25,7 +25,7 @@ SelectionMenuItem gridViewMenuItem(DocumentBloc documentBloc) =>
final service = AppBackendService();
final result = (await service.createView(
appId: appId,
parentViewId: appId,
name: LocaleKeys.menuAppHeader_defaultNewPageName.tr(),
layoutType: ViewLayoutPB.Grid,
))

View File

@ -104,7 +104,7 @@ class AppBloc extends Bloc<AppEvent, AppState> {
Future<void> _createView(CreateView value, Emitter<AppState> emit) async {
final result = await appService.createView(
appId: state.view.id,
parentViewId: state.view.id,
name: value.name,
desc: value.desc ?? "",
layoutType: value.pluginBuilder.layoutType!,

View File

@ -8,7 +8,7 @@ import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
class AppBackendService {
Future<Either<ViewPB, FlowyError>> createView({
required String appId,
required String parentViewId,
required String name,
String? desc,
required ViewLayoutPB layoutType,
@ -25,7 +25,7 @@ class AppBackendService {
Map<String, String> ext = const {},
}) {
final payload = CreateViewPayloadPB.create()
..belongToId = appId
..parentViewId = parentViewId
..name = name
..desc = desc ?? ""
..layout = layoutType

View File

@ -0,0 +1,40 @@
import 'dart:convert';
import 'dart:typed_data';
import 'package:appflowy_backend/dispatch/dispatch.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/view.pbenum.dart';
import 'package:dartz/dartz.dart';
class ImportBackendService {
static Future<Either<Unit, FlowyError>> importHistoryDatabase(
String 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 parentViewId,
) async {
final payload = ImportPB.create()
..data = data
..parentViewId = parentViewId
..viewLayout = ViewLayoutPB.Document
..name = name
..importType = ImportTypePB.HistoryDocument;
return await FolderEventImportData(payload).send();
}
}

View File

@ -25,7 +25,7 @@ class WorkspaceService {
String? desc,
}) {
final payload = CreateViewPayloadPB.create()
..belongToId = workspaceId
..parentViewId = workspaceId
..name = name
..desc = desc ?? ""
..layout = ViewLayoutPB.Document;

View File

@ -12,6 +12,7 @@ import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:easy_localization/easy_localization.dart';
class AddButton extends StatelessWidget {
final String parentViewId;
final Function(
PluginBuilder,
String? name,
@ -20,6 +21,7 @@ class AddButton extends StatelessWidget {
) onSelected;
const AddButton({
required this.parentViewId,
Key? key,
required this.onSelected,
}) : super(key: key);
@ -75,20 +77,34 @@ class AddButton extends StatelessWidget {
onSelected(action.pluginBuilder, null, null, true);
}
if (action is ImportActionWrapper) {
showImportPanel(context, (type, name, initialDataBytes) {
if (initialDataBytes == null) {
return;
}
switch (type) {
case ImportType.historyDocument:
case ImportType.historyDatabase:
onSelected(action.pluginBuilder, name, initialDataBytes, false);
break;
case ImportType.markdownOrText:
onSelected(action.pluginBuilder, name, initialDataBytes, true);
break;
}
});
showImportPanel(
parentViewId,
context,
(type, name, initialDataBytes) {
if (initialDataBytes == null) {
return;
}
switch (type) {
case ImportType.historyDocument:
case ImportType.historyDatabase:
onSelected(
action.pluginBuilder,
name,
initialDataBytes,
false,
);
break;
case ImportType.markdownOrText:
onSelected(
action.pluginBuilder,
name,
initialDataBytes,
true,
);
break;
}
},
);
}
controller.close();
},

View File

@ -17,9 +17,9 @@ import '../menu_app.dart';
import 'add_button.dart';
class MenuAppHeader extends StatelessWidget {
final ViewPB app;
final ViewPB parentView;
const MenuAppHeader(
this.app, {
this.parentView, {
Key? key,
}) : super(key: key);
@ -108,6 +108,7 @@ class MenuAppHeader extends StatelessWidget {
return Tooltip(
message: LocaleKeys.menuAppHeader_addPageTooltip.tr(),
child: AddButton(
parentViewId: parentView.id,
onSelected: (pluginBuilder, name, initialDataBytes, openAfterCreated) {
context.read<AppBloc>().add(
AppEvent.createView(

View File

@ -1,9 +1,11 @@
import 'dart:io';
import 'dart:typed_data';
import 'package:appflowy/plugins/document/application/document_data_pb_extension.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/migration/editor_migration.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/util/file_picker/file_picker_service.dart';
import 'package:appflowy/workspace/application/settings/share/import_service.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:file_picker/file_picker.dart';
import 'package:flowy_infra/image.dart';
@ -21,6 +23,7 @@ typedef ImportCallback = void Function(
);
Future<void> showImportPanel(
String parentViewId,
BuildContext context,
ImportCallback callback,
) async {
@ -33,7 +36,10 @@ Future<void> showImportPanel(
fontSize: 20,
color: Theme.of(context).colorScheme.tertiary,
),
content: _ImportPanel(importCallback: callback),
content: _ImportPanel(
parentViewId: parentViewId,
importCallback: callback,
),
contentPadding: const EdgeInsets.symmetric(
vertical: 10.0,
horizontal: 20.0,
@ -112,9 +118,11 @@ enum ImportType {
class _ImportPanel extends StatefulWidget {
const _ImportPanel({
required this.parentViewId,
required this.importCallback,
});
final String parentViewId;
final ImportCallback importCallback;
@override
@ -145,7 +153,7 @@ class _ImportPanelState extends State<_ImportPanel> {
overflow: TextOverflow.ellipsis,
),
onTap: () async {
await _importFile(e);
await _importFile(widget.parentViewId, e);
if (mounted) {
Navigator.of(context).pop();
}
@ -158,7 +166,7 @@ class _ImportPanelState extends State<_ImportPanel> {
);
}
Future<void> _importFile(ImportType importType) async {
Future<void> _importFile(String parentViewId, ImportType importType) async {
final result = await getIt<FilePickerService>().pickFiles(
allowMultiple: importType.allowMultiSelect,
type: FileType.custom,
@ -173,26 +181,45 @@ class _ImportPanelState extends State<_ImportPanel> {
if (path == null) {
continue;
}
final plainText = await File(path).readAsString();
Document? document;
final data = await File(path).readAsString();
final name = p.basenameWithoutExtension(path);
switch (importType) {
case ImportType.markdownOrText:
document = markdownToDocument(plainText);
break;
case ImportType.historyDocument:
document = EditorMigration.migrateDocument(plainText);
final bytes = _documentDataFrom(importType, data);
if (bytes != null) {
await ImportBackendService.importHistoryDocument(
bytes,
name,
parentViewId,
);
}
break;
case ImportType.historyDatabase:
await ImportBackendService.importHistoryDatabase(
data,
name,
parentViewId,
);
break;
default:
assert(false, 'Unsupported Type $importType');
}
if (document != null) {
final data = DocumentDataPBFromTo.fromDocument(document);
widget.importCallback(
importType,
p.basenameWithoutExtension(path),
data?.writeToBuffer(),
);
}
}
}
}
Uint8List? _documentDataFrom(ImportType importType, String data) {
switch (importType) {
case ImportType.markdownOrText:
final document = markdownToDocument(data);
return DocumentDataPBFromTo.fromDocument(document)?.writeToBuffer();
case ImportType.historyDocument:
final document = EditorMigration.migrateDocument(data);
return DocumentDataPBFromTo.fromDocument(document)?.writeToBuffer();
default:
assert(false, 'Unsupported Type $importType');
return null;
}
}

View File

@ -32,7 +32,7 @@ class AppFlowyBoardTest {
final builder = BoardPluginBuilder();
return AppBackendService()
.createView(
appId: app.id,
parentViewId: app.id,
name: "Test Board",
layoutType: builder.layoutType!,
)

View File

@ -10,7 +10,7 @@ Future<GridTestContext> createTestFilterGrid(AppFlowyGridTest gridTest) async {
final builder = GridPluginBuilder();
final context = await AppBackendService()
.createView(
appId: app.id,
parentViewId: app.id,
name: "Filter Grid",
layoutType: builder.layoutType!,
)

View File

@ -172,7 +172,7 @@ class AppFlowyGridTest {
final builder = GridPluginBuilder();
final context = await AppBackendService()
.createView(
appId: app.id,
parentViewId: app.id,
name: "Test Grid",
layoutType: builder.layoutType!,
)

View File

@ -36,7 +36,7 @@ export class AppBackendService {
}) => {
const encoder = new TextEncoder();
const payload = CreateViewPayloadPB.fromObject({
belong_to_id: this.appId,
parent_view_id: this.appId,
name: params.name,
desc: params.desc || '',
layout: params.layoutType,

View File

@ -19,7 +19,7 @@ export class WorkspaceBackendService {
createApp = async (params: { name: string; desc?: string }) => {
const payload = CreateViewPayloadPB.fromObject({
belong_to_id: this.workspaceId,
parent_view_id: this.workspaceId,
name: params.name,
desc: params.desc || '',
layout: ViewLayoutPB.Document,

View File

@ -7,6 +7,7 @@ use appflowy_integrate::RocksCollabDB;
use bytes::Bytes;
use flowy_database2::entities::DatabaseLayoutPB;
use flowy_database2::services::share::csv::CSVFormat;
use flowy_database2::template::{make_default_board, make_default_calendar, make_default_grid};
use flowy_database2::DatabaseManager2;
use flowy_document2::document_data::DocumentDataWrapper;
@ -16,7 +17,7 @@ use flowy_error::FlowyError;
use flowy_folder2::deps::{FolderCloudService, FolderUser};
use flowy_folder2::entities::ViewLayoutPB;
use flowy_folder2::manager::Folder2Manager;
use flowy_folder2::view_ext::{ViewDataProcessor, ViewDataProcessorMap};
use flowy_folder2::view_ext::{FolderOperationHandler, FolderOperationHandlers};
use flowy_folder2::ViewLayout;
use flowy_user::services::UserSession;
use lib_dispatch::prelude::ToBytes;
@ -33,29 +34,28 @@ impl Folder2DepsResolver {
) -> Arc<Folder2Manager> {
let user: Arc<dyn FolderUser> = Arc::new(FolderUserImpl(user_session.clone()));
let view_processors =
make_view_data_processor(document_manager.clone(), database_manager.clone());
let handlers = folder_operation_handlers(document_manager.clone(), database_manager.clone());
Arc::new(
Folder2Manager::new(user.clone(), collab_builder, view_processors, folder_cloud)
Folder2Manager::new(user.clone(), collab_builder, handlers, folder_cloud)
.await
.unwrap(),
)
}
}
fn make_view_data_processor(
fn folder_operation_handlers(
document_manager: Arc<DocumentManager>,
database_manager: Arc<DatabaseManager2>,
) -> ViewDataProcessorMap {
let mut map: HashMap<ViewLayout, Arc<dyn ViewDataProcessor + Send + Sync>> = HashMap::new();
) -> FolderOperationHandlers {
let mut map: HashMap<ViewLayout, Arc<dyn FolderOperationHandler + Send + Sync>> = HashMap::new();
let document_processor = Arc::new(DocumentViewDataProcessor(document_manager));
map.insert(ViewLayout::Document, document_processor);
let document_folder_operation = Arc::new(DocumentFolderOperation(document_manager));
map.insert(ViewLayout::Document, document_folder_operation);
let database_processor = Arc::new(DatabaseViewDataProcessor(database_manager));
map.insert(ViewLayout::Board, database_processor.clone());
map.insert(ViewLayout::Grid, database_processor.clone());
map.insert(ViewLayout::Calendar, database_processor);
let database_folder_operation = Arc::new(DatabaseFolderOperation(database_manager));
map.insert(ViewLayout::Board, database_folder_operation.clone());
map.insert(ViewLayout::Grid, database_folder_operation.clone());
map.insert(ViewLayout::Calendar, database_folder_operation);
Arc::new(map)
}
@ -80,8 +80,8 @@ impl FolderUser for FolderUserImpl {
}
}
struct DocumentViewDataProcessor(Arc<DocumentManager>);
impl ViewDataProcessor for DocumentViewDataProcessor {
struct DocumentFolderOperation(Arc<DocumentManager>);
impl FolderOperationHandler for DocumentFolderOperation {
/// Close the document view.
fn close_view(&self, view_id: &str) -> FutureResult<(), FlowyError> {
let manager = self.0.clone();
@ -92,10 +92,7 @@ impl ViewDataProcessor for DocumentViewDataProcessor {
})
}
/// Get the view data.
///
/// only use in the duplicate view.
fn get_view_data(&self, view_id: &str) -> FutureResult<Bytes, FlowyError> {
fn duplicate_view(&self, view_id: &str) -> FutureResult<Bytes, FlowyError> {
let manager = self.0.clone();
let view_id = view_id.to_string();
FutureResult::new(async move {
@ -106,27 +103,7 @@ impl ViewDataProcessor for DocumentViewDataProcessor {
})
}
/// Create a view with built-in data.
fn create_view_with_built_in_data(
&self,
_user_id: i64,
view_id: &str,
_name: &str,
layout: ViewLayout,
_ext: HashMap<String, String>,
) -> FutureResult<(), FlowyError> {
debug_assert_eq!(layout, ViewLayout::Document);
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())?;
Ok(())
})
}
fn create_view_with_custom_data(
fn create_view_with_view_data(
&self,
_user_id: i64,
view_id: &str,
@ -145,10 +122,55 @@ impl ViewDataProcessor for DocumentViewDataProcessor {
Ok(())
})
}
/// Create a view with built-in data.
fn create_built_in_view(
&self,
_user_id: i64,
view_id: &str,
_name: &str,
layout: ViewLayout,
_ext: HashMap<String, String>,
) -> FutureResult<(), FlowyError> {
debug_assert_eq!(layout, ViewLayout::Document);
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())?;
Ok(())
})
}
fn import_from_bytes(
&self,
view_id: &str,
_name: &str,
bytes: Vec<u8>,
) -> FutureResult<(), FlowyError> {
let view_id = view_id.to_string();
let manager = self.0.clone();
FutureResult::new(async move {
let data = DocumentDataPB::try_from(Bytes::from(bytes))?;
manager.create_document(view_id, data.into())?;
Ok(())
})
}
// will implement soon
fn import_from_file_path(
&self,
_view_id: &str,
_name: &str,
_path: String,
) -> FutureResult<(), FlowyError> {
FutureResult::new(async move { Ok(()) })
}
}
struct DatabaseViewDataProcessor(Arc<DatabaseManager2>);
impl ViewDataProcessor for DatabaseViewDataProcessor {
struct DatabaseFolderOperation(Arc<DatabaseManager2>);
impl FolderOperationHandler for DatabaseFolderOperation {
fn close_view(&self, view_id: &str) -> FutureResult<(), FlowyError> {
let database_manager = self.0.clone();
let view_id = view_id.to_string();
@ -158,7 +180,7 @@ impl ViewDataProcessor for DatabaseViewDataProcessor {
})
}
fn get_view_data(&self, view_id: &str) -> FutureResult<Bytes, FlowyError> {
fn duplicate_view(&self, view_id: &str) -> FutureResult<Bytes, FlowyError> {
let database_manager = self.0.clone();
let view_id = view_id.to_owned();
FutureResult::new(async move {
@ -167,40 +189,10 @@ impl ViewDataProcessor for DatabaseViewDataProcessor {
})
}
/// Create a database view with build-in data.
/// If the ext contains the {"database_id": "xx"}, then it will link to
/// the existing database. The data of the database will be shared within
/// these references views.
fn create_view_with_built_in_data(
&self,
_user_id: i64,
view_id: &str,
name: &str,
layout: ViewLayout,
_ext: HashMap<String, String>,
) -> FutureResult<(), FlowyError> {
let name = name.to_string();
let database_manager = self.0.clone();
let data = match layout {
ViewLayout::Grid => make_default_grid(view_id, &name),
ViewLayout::Board => make_default_board(view_id, &name),
ViewLayout::Calendar => make_default_calendar(view_id, &name),
ViewLayout::Document => {
return FutureResult::new(async move {
Err(FlowyError::internal().context(format!("Can't handle {:?} layout type", layout)))
});
},
};
FutureResult::new(async move {
database_manager.create_database_with_params(data).await?;
Ok(())
})
}
/// Create a database view with duplicated data.
/// If the ext contains the {"database_id": "xx"}, then it will link
/// to the existing database.
fn create_view_with_custom_data(
fn create_view_with_view_data(
&self,
_user_id: i64,
view_id: &str,
@ -241,6 +233,68 @@ impl ViewDataProcessor for DatabaseViewDataProcessor {
},
}
}
/// Create a database view with build-in data.
/// If the ext contains the {"database_id": "xx"}, then it will link to
/// the existing database. The data of the database will be shared within
/// these references views.
fn create_built_in_view(
&self,
_user_id: i64,
view_id: &str,
name: &str,
layout: ViewLayout,
_meta: HashMap<String, String>,
) -> FutureResult<(), FlowyError> {
let name = name.to_string();
let database_manager = self.0.clone();
let data = match layout {
ViewLayout::Grid => make_default_grid(view_id, &name),
ViewLayout::Board => make_default_board(view_id, &name),
ViewLayout::Calendar => make_default_calendar(view_id, &name),
ViewLayout::Document => {
return FutureResult::new(async move {
Err(FlowyError::internal().context(format!("Can't handle {:?} layout type", layout)))
});
},
};
FutureResult::new(async move {
database_manager.create_database_with_params(data).await?;
Ok(())
})
}
fn import_from_bytes(
&self,
view_id: &str,
_name: &str,
bytes: Vec<u8>,
) -> FutureResult<(), FlowyError> {
let database_manager = self.0.clone();
let view_id = view_id.to_string();
FutureResult::new(async move {
let content = String::from_utf8(bytes).map_err(|err| FlowyError::internal().context(err))?;
database_manager
.import_csv(view_id, content, CSVFormat::META)
.await?;
Ok(())
})
}
fn import_from_file_path(
&self,
_view_id: &str,
_name: &str,
path: String,
) -> FutureResult<(), FlowyError> {
let database_manager = self.0.clone();
FutureResult::new(async move {
database_manager
.import_csv_from_file(path, CSVFormat::META)
.await?;
Ok(())
})
}
}
#[derive(Debug, serde::Deserialize)]

View File

@ -1,361 +0,0 @@
use bytes::Bytes;
use flowy_sqlite::ConnectionPool;
use database_model::BuildDatabaseContext;
use flowy_client_ws::FlowyWebSocketConnect;
use flowy_database::entities::LayoutTypePB;
use flowy_database::manager::{create_new_database, link_existing_database, DatabaseManager};
use flowy_database::util::{make_default_board, make_default_calendar, make_default_grid};
use flowy_document::editor::make_transaction_from_document_content;
use flowy_document::DocumentManager;
use flowy_folder::entities::{ViewDataFormatPB, ViewLayoutTypePB, ViewPB};
use flowy_folder::manager::{ViewDataProcessor, ViewDataProcessorMap};
use flowy_folder::{
errors::{internal_error, FlowyError},
event_map::{FolderCouldServiceV1, WorkspaceDatabase, WorkspaceUser},
manager::FolderManager,
};
use flowy_net::ClientServerConfiguration;
use flowy_net::{http_server::folder::FolderHttpCloudService, local_server::LocalServer};
use flowy_revision::{RevisionWebSocket, WSStateReceiver};
use flowy_user::services::UserSession;
use futures_core::future::BoxFuture;
use lib_infra::future::{BoxResultFuture, FutureResult};
use lib_ws::{WSChannel, WSMessageReceiver, WebSocketRawMessage};
use revision_model::Revision;
use std::collections::HashMap;
use std::convert::TryFrom;
use std::{convert::TryInto, sync::Arc};
use ws_model::ws_revision::ClientRevisionWSData;
pub struct FolderDepsResolver();
impl FolderDepsResolver {
pub async fn resolve(
local_server: Option<Arc<LocalServer>>,
user_session: Arc<UserSession>,
server_config: &ClientServerConfiguration,
ws_conn: &Arc<FlowyWebSocketConnect>,
text_block_manager: &Arc<DocumentManager>,
database_manager: &Arc<DatabaseManager>,
) -> Arc<FolderManager> {
let user: Arc<dyn WorkspaceUser> = Arc::new(WorkspaceUserImpl(user_session.clone()));
let database: Arc<dyn WorkspaceDatabase> = Arc::new(WorkspaceDatabaseImpl(user_session));
let web_socket = Arc::new(FolderRevisionWebSocket(ws_conn.clone()));
let cloud_service: Arc<dyn FolderCouldServiceV1> = match local_server {
None => Arc::new(FolderHttpCloudService::new(server_config.clone())),
Some(local_server) => local_server,
};
let view_data_processor =
make_view_data_processor(text_block_manager.clone(), database_manager.clone());
let folder_manager = Arc::new(
FolderManager::new(
user.clone(),
cloud_service,
database,
view_data_processor,
web_socket,
)
.await,
);
// if let (Ok(user_id), Ok(token)) = (user.user_id(), user.token()) {
// match folder_manager.initialize(&user_id, &token).await {
// Ok(_) => {},
// Err(e) => tracing::error!("Initialize folder manager failed: {}", e),
// }
// }
let receiver = Arc::new(FolderWSMessageReceiverImpl(folder_manager.clone()));
ws_conn.add_ws_message_receiver(receiver).unwrap();
folder_manager
}
}
fn make_view_data_processor(
document_manager: Arc<DocumentManager>,
database_manager: Arc<DatabaseManager>,
) -> ViewDataProcessorMap {
let mut map: HashMap<ViewDataFormatPB, Arc<dyn ViewDataProcessor + Send + Sync>> = HashMap::new();
let document_processor = Arc::new(DocumentViewDataProcessor(document_manager));
document_processor
.data_types()
.into_iter()
.for_each(|data_type| {
map.insert(data_type, document_processor.clone());
});
let grid_data_impl = Arc::new(DatabaseViewDataProcessor(database_manager));
grid_data_impl
.data_types()
.into_iter()
.for_each(|data_type| {
map.insert(data_type, grid_data_impl.clone());
});
Arc::new(map)
}
struct WorkspaceDatabaseImpl(Arc<UserSession>);
impl WorkspaceDatabase for WorkspaceDatabaseImpl {
fn db_pool(&self) -> Result<Arc<ConnectionPool>, FlowyError> {
self
.0
.db_pool()
.map_err(|e| FlowyError::internal().context(e))
}
}
struct WorkspaceUserImpl(Arc<UserSession>);
impl WorkspaceUser for WorkspaceUserImpl {
fn user_id(&self) -> Result<String, FlowyError> {
self
.0
.user_id()
.map_err(|e| FlowyError::internal().context(e))
}
fn token(&self) -> Result<Option<String>, FlowyError> {
self
.0
.token()
.map_err(|e| FlowyError::internal().context(e))
}
}
struct FolderRevisionWebSocket(Arc<FlowyWebSocketConnect>);
impl RevisionWebSocket for FolderRevisionWebSocket {
fn send(&self, data: ClientRevisionWSData) -> BoxResultFuture<(), FlowyError> {
let bytes: Bytes = data.try_into().unwrap();
let msg = WebSocketRawMessage {
channel: WSChannel::Folder,
data: bytes.to_vec(),
};
let ws_conn = self.0.clone();
Box::pin(async move {
match ws_conn.web_socket().await? {
None => {},
Some(sender) => {
sender.send(msg).map_err(internal_error)?;
},
}
Ok(())
})
}
fn subscribe_state_changed(&self) -> BoxFuture<WSStateReceiver> {
let ws_conn = self.0.clone();
Box::pin(async move { ws_conn.subscribe_websocket_state().await })
}
}
struct FolderWSMessageReceiverImpl(Arc<FolderManager>);
impl WSMessageReceiver for FolderWSMessageReceiverImpl {
fn source(&self) -> WSChannel {
WSChannel::Folder
}
fn receive_message(&self, msg: WebSocketRawMessage) {
let handler = self.0.clone();
tokio::spawn(async move {
handler.did_receive_ws_data(Bytes::from(msg.data)).await;
});
}
}
struct DocumentViewDataProcessor(Arc<DocumentManager>);
impl ViewDataProcessor for DocumentViewDataProcessor {
fn close_view(&self, view_id: &str) -> FutureResult<(), FlowyError> {
let manager = self.0.clone();
let view_id = view_id.to_string();
FutureResult::new(async move {
manager.close_document_editor(view_id).await?;
Ok(())
})
}
fn get_view_data(&self, view: &ViewPB) -> FutureResult<Bytes, FlowyError> {
let view_id = view.id.clone();
let manager = self.0.clone();
FutureResult::new(async move {
let editor = manager.open_document_editor(view_id).await?;
let document_data = Bytes::from(editor.duplicate().await?);
Ok(document_data)
})
}
fn create_view_with_built_in_data(
&self,
user_id: &str,
view_id: &str,
_name: &str,
layout: ViewLayoutTypePB,
_data_format: ViewDataFormatPB,
_ext: HashMap<String, String>,
) -> FutureResult<(), FlowyError> {
debug_assert_eq!(layout, ViewLayoutTypePB::Document);
let _user_id = user_id.to_string();
let view_id = view_id.to_string();
let manager = self.0.clone();
// todo: implement the default content
FutureResult::new(async move {
let delta_data = Bytes::from(document_content);
let revision = Revision::initial_revision(&view_id, delta_data);
manager.create_document(view_id, vec![revision]).await?;
Ok(())
})
}
fn create_view_with_custom_data(
&self,
_user_id: &str,
view_id: &str,
_name: &str,
data: Vec<u8>,
layout: ViewLayoutTypePB,
_ext: HashMap<String, String>,
) -> FutureResult<(), FlowyError> {
debug_assert_eq!(layout, ViewLayoutTypePB::Document);
let view_data = match String::from_utf8(data) {
Ok(content) => match make_transaction_from_document_content(&content) {
Ok(transaction) => transaction.to_bytes().unwrap_or_else(|_| vec![]),
Err(_) => vec![],
},
Err(_) => vec![],
};
let revision = Revision::initial_revision(view_id, Bytes::from(view_data));
let view_id = view_id.to_string();
let manager = self.0.clone();
FutureResult::new(async move {
manager.create_document(view_id, vec![revision]).await?;
Ok(())
})
}
fn data_types(&self) -> Vec<ViewDataFormatPB> {
vec![ViewDataFormatPB::DeltaFormat, ViewDataFormatPB::NodeFormat]
}
}
struct DatabaseViewDataProcessor(Arc<DatabaseManager>);
impl ViewDataProcessor for DatabaseViewDataProcessor {
fn close_view(&self, view_id: &str) -> FutureResult<(), FlowyError> {
let database_manager = self.0.clone();
let view_id = view_id.to_string();
FutureResult::new(async move {
database_manager.close_database_view(view_id).await?;
Ok(())
})
}
fn get_view_data(&self, view: &ViewPB) -> FutureResult<Bytes, FlowyError> {
let database_manager = self.0.clone();
let view_id = view.id.clone();
FutureResult::new(async move {
let editor = database_manager.open_database_view(&view_id).await?;
let delta_bytes = editor.duplicate_database(&view_id).await?;
Ok(delta_bytes.into())
})
}
/// Create a database view with build-in data.
/// If the ext contains the {"database_id": "xx"}, then it will link to
/// the existing database. The data of the database will be shared within
/// these references views.
fn create_view_with_built_in_data(
&self,
_user_id: &str,
view_id: &str,
name: &str,
layout: ViewLayoutTypePB,
data_format: ViewDataFormatPB,
ext: HashMap<String, String>,
) -> FutureResult<(), FlowyError> {
debug_assert_eq!(data_format, ViewDataFormatPB::DatabaseFormat);
let view_id = view_id.to_string();
let name = name.to_string();
let database_manager = self.0.clone();
match DatabaseExtParams::from_map(ext).map(|params| params.database_id) {
None => {
let (build_context, layout) = match layout {
ViewLayoutTypePB::Grid => (make_default_grid(), LayoutTypePB::Grid),
ViewLayoutTypePB::Board => (make_default_board(), LayoutTypePB::Board),
ViewLayoutTypePB::Calendar => (make_default_calendar(), LayoutTypePB::Calendar),
ViewLayoutTypePB::Document => {
return FutureResult::new(async move {
Err(FlowyError::internal().context(format!("Can't handle {:?} layout type", layout)))
});
},
};
FutureResult::new(async move {
create_new_database(&view_id, name, layout, database_manager, build_context).await
})
},
Some(database_id) => {
let layout = layout_type_from_view_layout(layout);
FutureResult::new(async move {
link_existing_database(&view_id, name, &database_id, layout, database_manager).await
})
},
}
}
/// Create a database view with custom data.
/// If the ext contains the {"database_id": "xx"}, then it will link
/// to the existing database. The data of the database will be shared
/// within these references views.
fn create_view_with_custom_data(
&self,
_user_id: &str,
view_id: &str,
name: &str,
data: Vec<u8>,
layout: ViewLayoutTypePB,
ext: HashMap<String, String>,
) -> FutureResult<(), FlowyError> {
let view_id = view_id.to_string();
let database_manager = self.0.clone();
let layout = layout_type_from_view_layout(layout);
let name = name.to_string();
match DatabaseExtParams::from_map(ext).map(|params| params.database_id) {
None => FutureResult::new(async move {
let bytes = Bytes::from(data);
let build_context = BuildDatabaseContext::try_from(bytes)?;
let _ = create_new_database(&view_id, name, layout, database_manager, build_context).await;
Ok(())
}),
Some(database_id) => FutureResult::new(async move {
link_existing_database(&view_id, name, &database_id, layout, database_manager).await
}),
}
}
fn data_types(&self) -> Vec<ViewDataFormatPB> {
vec![ViewDataFormatPB::DatabaseFormat]
}
}
pub fn layout_type_from_view_layout(layout: ViewLayoutTypePB) -> LayoutTypePB {
match layout {
ViewLayoutTypePB::Grid => LayoutTypePB::Grid,
ViewLayoutTypePB::Board => LayoutTypePB::Board,
ViewLayoutTypePB::Calendar => LayoutTypePB::Calendar,
ViewLayoutTypePB::Document => LayoutTypePB::Grid,
}
}
#[derive(Debug, serde::Deserialize)]
struct DatabaseExtParams {
database_id: String,
}
impl DatabaseExtParams {
pub fn from_map(map: HashMap<String, String>) -> Option<Self> {
let value = serde_json::to_value(map).ok()?;
serde_json::from_value::<Self>(value).ok()
}
}

View File

@ -12,7 +12,6 @@ mod view_entities;
#[macro_use]
mod macros;
mod share_entities;
mod type_option_entities;
pub use calendar_entities::*;
@ -23,7 +22,6 @@ pub use filter_entities::*;
pub use group_entities::*;
pub use row_entities::*;
pub use setting_entities::*;
pub use share_entities::*;
pub use sort_entities::*;
pub use type_option_entities::*;
pub use view_entities::*;

View File

@ -1,24 +0,0 @@
use flowy_derive::{ProtoBuf, ProtoBuf_Enum};
#[derive(Clone, Debug, ProtoBuf_Enum)]
pub enum ImportTypePB {
CSV = 0,
}
impl Default for ImportTypePB {
fn default() -> Self {
Self::CSV
}
}
#[derive(Clone, Debug, ProtoBuf, Default)]
pub struct DatabaseImportPB {
#[pb(index = 1, one_of)]
pub data: Option<String>,
#[pb(index = 2, one_of)]
pub uri: Option<String>,
#[pb(index = 3)]
pub import_type: ImportTypePB,
}

View File

@ -5,7 +5,7 @@ use collab_database::rows::RowId;
use collab_database::views::DatabaseLayout;
use lib_infra::util::timestamp;
use flowy_error::{ErrorCode, FlowyError, FlowyResult};
use flowy_error::{FlowyError, FlowyResult};
use lib_dispatch::prelude::{data_result_ok, AFPluginData, AFPluginState, DataResult};
use crate::entities::*;
@ -17,7 +17,6 @@ use crate::services::field::{
type_option_data_from_pb_or_default, DateCellChangeset, SelectOptionCellChangeset,
};
use crate::services::group::{GroupChangeset, GroupSettingChangeset};
use crate::services::share::csv::CSVFormat;
#[tracing::instrument(level = "trace", skip_all, err)]
pub(crate) async fn get_database_data_handler(
@ -678,27 +677,3 @@ pub(crate) async fn get_calendar_event_handler(
Some(event) => data_result_ok(event),
}
}
#[tracing::instrument(level = "debug", skip(data, manager), err)]
pub(crate) async fn import_data_handler(
data: AFPluginData<DatabaseImportPB>,
manager: AFPluginState<Arc<DatabaseManager2>>,
) -> FlowyResult<()> {
let params = data.into_inner();
match params.import_type {
ImportTypePB::CSV => {
if let Some(data) = params.data {
manager.import_csv(data, CSVFormat::META).await?;
} else if let Some(uri) = params.uri {
manager.import_csv_from_uri(uri, CSVFormat::META).await?;
} else {
return Err(FlowyError::new(
ErrorCode::InvalidData,
"No data or uri provided",
));
}
},
}
Ok(())
}

View File

@ -9,10 +9,10 @@ use crate::event_handler::*;
use crate::manager::DatabaseManager2;
pub fn init(database_manager: Arc<DatabaseManager2>) -> AFPlugin {
let mut plugin = AFPlugin::new()
let plugin = AFPlugin::new()
.name(env!("CARGO_PKG_NAME"))
.state(database_manager);
plugin = plugin
plugin
.event(DatabaseEvent::GetDatabase, get_database_data_handler)
.event(DatabaseEvent::GetDatabaseSetting, get_database_setting_handler)
.event(DatabaseEvent::UpdateDatabaseSetting, update_database_setting_handler)
@ -64,10 +64,7 @@ pub fn init(database_manager: Arc<DatabaseManager2>) -> AFPlugin {
// Layout setting
.event(DatabaseEvent::SetLayoutSetting, set_layout_setting_handler)
.event(DatabaseEvent::GetLayoutSetting, get_layout_setting_handler)
// import
.event(DatabaseEvent::ImportCSV, import_data_handler);
plugin
// import
}
/// [DatabaseEvent] defines events that are used to interact with the Grid. You could check [this](https://appflowy.gitbook.io/docs/essential-documentation/contribute-to-appflowy/architecture/backend/protobuf)
@ -278,7 +275,4 @@ pub enum DatabaseEvent {
#[event(input = "MoveCalendarEventPB")]
MoveCalendarEvent = 125,
#[event(input = "DatabaseImportPB")]
ImportCSV = 130,
}

View File

@ -195,11 +195,17 @@ impl DatabaseManager2 {
Ok(())
}
pub async fn import_csv(&self, content: String, format: CSVFormat) -> FlowyResult<ImportResult> {
let params =
tokio::task::spawn_blocking(move || CSVImporter.import_csv_from_string(content, format))
.await
.map_err(internal_error)??;
pub async fn import_csv(
&self,
view_id: String,
content: String,
format: CSVFormat,
) -> FlowyResult<ImportResult> {
let params = tokio::task::spawn_blocking(move || {
CSVImporter.import_csv_from_string(view_id, content, format)
})
.await
.map_err(internal_error)??;
let result = ImportResult {
database_id: params.database_id.clone(),
view_id: params.view_id.clone(),
@ -208,7 +214,12 @@ impl DatabaseManager2 {
Ok(result)
}
pub async fn import_csv_from_uri(&self, _uri: String, _format: CSVFormat) -> FlowyResult<()> {
// will implement soon
pub async fn import_csv_from_file(
&self,
_file_path: String,
_format: CSVFormat,
) -> FlowyResult<()> {
Ok(())
}

View File

@ -2,7 +2,7 @@ use crate::entities::FieldType;
use crate::services::field::{default_type_option_data_from_type, CELL_DATA};
use crate::services::share::csv::CSVFormat;
use collab_database::database::{gen_database_id, gen_database_view_id, gen_field_id, gen_row_id};
use collab_database::database::{gen_database_id, gen_field_id, gen_row_id};
use collab_database::fields::Field;
use collab_database::rows::{new_cell_builder, Cell, CreateRowParams};
use collab_database::views::{CreateDatabaseParams, DatabaseLayout};
@ -15,6 +15,7 @@ pub struct CSVImporter;
impl CSVImporter {
pub fn import_csv_from_file(
&self,
view_id: &str,
path: &str,
style: CSVFormat,
) -> FlowyResult<CreateDatabaseParams> {
@ -22,17 +23,18 @@ impl CSVImporter {
let mut content = String::new();
file.read_to_string(&mut content)?;
let fields_with_rows = self.get_fields_and_rows(content)?;
let database_data = database_from_fields_and_rows(fields_with_rows, &style);
let database_data = database_from_fields_and_rows(view_id, fields_with_rows, &style);
Ok(database_data)
}
pub fn import_csv_from_string(
&self,
view_id: String,
content: String,
format: CSVFormat,
) -> FlowyResult<CreateDatabaseParams> {
let fields_with_rows = self.get_fields_and_rows(content)?;
let database_data = database_from_fields_and_rows(fields_with_rows, &format);
let database_data = database_from_fields_and_rows(&view_id, fields_with_rows, &format);
Ok(database_data)
}
@ -68,11 +70,11 @@ impl CSVImporter {
}
fn database_from_fields_and_rows(
view_id: &str,
fields_and_rows: FieldsRows,
format: &CSVFormat,
) -> CreateDatabaseParams {
let (fields, rows) = fields_and_rows.split();
let view_id = gen_database_view_id();
let database_id = gen_database_id();
let fields = fields
@ -125,7 +127,7 @@ fn database_from_fields_and_rows(
CreateDatabaseParams {
database_id,
view_id,
view_id: view_id.to_string(),
name: "".to_string(),
layout: DatabaseLayout::Grid,
layout_settings: Default::default(),
@ -167,6 +169,7 @@ pub struct ImportResult {
#[cfg(test)]
mod tests {
use crate::services::share::csv::{CSVFormat, CSVImporter};
use collab_database::database::gen_database_view_id;
#[test]
fn test_import_csv_from_str() {
@ -176,7 +179,7 @@ mod tests {
,,,,Yes,"#;
let importer = CSVImporter;
let result = importer
.import_csv_from_string(s.to_string(), CSVFormat::Original)
.import_csv_from_string(gen_database_view_id(), s.to_string(), CSVFormat::Original)
.unwrap();
assert_eq!(result.created_rows.len(), 3);
assert_eq!(result.fields.len(), 6);

View File

@ -1,3 +1,4 @@
use collab_database::database::gen_database_view_id;
use std::collections::HashMap;
use std::sync::Arc;
@ -262,7 +263,7 @@ impl DatabaseEditorTest {
self
.sdk
.database_manager
.import_csv(s, format)
.import_csv(gen_database_view_id(), s, format)
.await
.unwrap()
}

View File

@ -0,0 +1,82 @@
use crate::entities::parser::empty_str::NotEmptyStr;
use crate::entities::ViewLayoutPB;
use crate::share::{ImportParams, ImportType};
use flowy_derive::{ProtoBuf, ProtoBuf_Enum};
use flowy_error::FlowyError;
#[derive(Clone, Debug, ProtoBuf_Enum)]
pub enum ImportTypePB {
HistoryDocument = 0,
HistoryDatabase = 1,
}
impl From<ImportTypePB> for ImportType {
fn from(pb: ImportTypePB) -> Self {
match pb {
ImportTypePB::HistoryDocument => ImportType::HistoryDocument,
ImportTypePB::HistoryDatabase => ImportType::HistoryDatabase,
}
}
}
impl Default for ImportTypePB {
fn default() -> Self {
Self::HistoryDocument
}
}
#[derive(Clone, Debug, ProtoBuf, Default)]
pub struct ImportPB {
#[pb(index = 1)]
pub parent_view_id: String,
#[pb(index = 2)]
pub name: String,
#[pb(index = 3, one_of)]
pub data: Option<Vec<u8>>,
#[pb(index = 4, one_of)]
pub file_path: Option<String>,
#[pb(index = 5)]
pub view_layout: ViewLayoutPB,
#[pb(index = 6)]
pub import_type: ImportTypePB,
}
impl TryInto<ImportParams> for ImportPB {
type Error = FlowyError;
fn try_into(self) -> Result<ImportParams, Self::Error> {
let parent_view_id = NotEmptyStr::parse(self.parent_view_id)
.map_err(|_| FlowyError::invalid_view_id())?
.0;
let name = if self.name.is_empty() {
"Untitled".to_string()
} else {
self.name
};
let file_path = match self.file_path {
None => None,
Some(file_path) => Some(
NotEmptyStr::parse(file_path)
.map_err(|_| FlowyError::invalid_data().context("The import file path is empty"))?
.0,
),
};
Ok(ImportParams {
parent_view_id,
name,
data: self.data,
file_path,
view_layout: self.view_layout.into(),
import_type: self.import_type.into(),
})
}
}

View File

@ -1,8 +1,10 @@
mod import;
mod parser;
pub mod trash;
pub mod view;
pub mod workspace;
pub use import::*;
pub use trash::*;
pub use view::*;
pub use workspace::*;

View File

@ -0,0 +1,17 @@
#[derive(Debug)]
pub struct NotEmptyStr(pub String);
impl NotEmptyStr {
pub fn parse(s: String) -> Result<Self, String> {
if s.trim().is_empty() {
return Err("Input string is empty".to_owned());
}
Ok(Self(s))
}
}
impl AsRef<str> for NotEmptyStr {
fn as_ref(&self) -> &str {
&self.0
}
}

View File

@ -1,4 +1,5 @@
// pub mod app;
pub mod empty_str;
pub mod trash;
pub mod view;
pub mod workspace;

View File

@ -123,7 +123,7 @@ pub struct RepeatedViewIdPB {
#[derive(Default, ProtoBuf)]
pub struct CreateViewPayloadPB {
#[pb(index = 1)]
pub belong_to_id: String,
pub parent_view_id: String,
#[pb(index = 2)]
pub name: String,
@ -146,13 +146,13 @@ pub struct CreateViewPayloadPB {
#[derive(Debug, Clone)]
pub struct CreateViewParams {
pub belong_to_id: String,
pub parent_view_id: String,
pub name: String,
pub desc: String,
pub layout: ViewLayoutPB,
pub view_id: String,
pub initial_data: Vec<u8>,
pub ext: HashMap<String, String>,
pub meta: HashMap<String, String>,
}
impl TryInto<CreateViewParams> for CreateViewPayloadPB {
@ -160,17 +160,17 @@ impl TryInto<CreateViewParams> for CreateViewPayloadPB {
fn try_into(self) -> Result<CreateViewParams, Self::Error> {
let name = ViewName::parse(self.name)?.0;
let belong_to_id = ViewIdentify::parse(self.belong_to_id)?.0;
let belong_to_id = ViewIdentify::parse(self.parent_view_id)?.0;
let view_id = gen_view_id();
Ok(CreateViewParams {
belong_to_id,
parent_view_id: belong_to_id,
name,
desc: self.desc,
layout: self.layout,
view_id,
initial_data: self.initial_data,
ext: self.ext,
meta: self.ext,
})
}
}

View File

@ -1,12 +1,13 @@
use crate::entities::{
view_pb_without_child_views, CreateViewParams, CreateViewPayloadPB, CreateWorkspaceParams,
CreateWorkspacePayloadPB, MoveFolderItemPayloadPB, MoveViewParams, RepeatedTrashIdPB,
CreateWorkspacePayloadPB, ImportPB, MoveFolderItemPayloadPB, MoveViewParams, RepeatedTrashIdPB,
RepeatedTrashPB, RepeatedViewIdPB, RepeatedViewPB, RepeatedWorkspacePB, TrashIdPB,
UpdateViewParams, UpdateViewPayloadPB, ViewIdPB, ViewPB, WorkspaceIdPB, WorkspacePB,
WorkspaceSettingPB,
};
use crate::manager::Folder2Manager;
use crate::share::ImportParams;
use flowy_error::FlowyError;
use lib_dispatch::prelude::{data_result_ok, AFPluginData, AFPluginState, DataResult};
use std::sync::Arc;
@ -203,3 +204,13 @@ pub(crate) async fn delete_all_trash_handler(
folder.delete_all_trash().await;
Ok(())
}
#[tracing::instrument(level = "debug", skip(data, folder), err)]
pub(crate) async fn import_data_handler(
data: AFPluginData<ImportPB>,
folder: AFPluginState<Arc<Folder2Manager>>,
) -> Result<(), FlowyError> {
let params: ImportParams = data.into_inner().try_into()?;
folder.import(params).await?;
Ok(())
}

View File

@ -33,6 +33,7 @@ pub fn init(folder: Arc<Folder2Manager>) -> AFPlugin {
.event(FolderEvent::DeleteTrash, delete_trash_handler)
.event(FolderEvent::RestoreAllTrash, restore_all_trash_handler)
.event(FolderEvent::DeleteAllTrash, delete_all_trash_handler)
.event(FolderEvent::ImportData, import_data_handler)
}
#[derive(Clone, Copy, PartialEq, Eq, Debug, Display, Hash, ProtoBuf_Enum, Flowy_Event)]
@ -64,57 +65,60 @@ pub enum FolderEvent {
/// Create a new view in the corresponding app
#[event(input = "CreateViewPayloadPB", output = "ViewPB")]
CreateView = 201,
CreateView = 10,
/// Return the view info
#[event(input = "ViewIdPB", output = "ViewPB")]
ReadView = 202,
ReadView = 11,
/// Update the view's properties including the name,description, etc.
#[event(input = "UpdateViewPayloadPB", output = "ViewPB")]
UpdateView = 203,
UpdateView = 12,
/// Move the view to the trash folder
#[event(input = "RepeatedViewIdPB")]
DeleteView = 204,
DeleteView = 13,
/// Duplicate the view
#[event(input = "ViewPB")]
DuplicateView = 205,
DuplicateView = 14,
/// Close and release the resources that are used by this view.
/// It should get called when the 'View' page get destroy
#[event(input = "ViewIdPB")]
CloseView = 206,
CloseView = 15,
#[event()]
CopyLink = 220,
CopyLink = 20,
/// Set the current visiting view
#[event(input = "ViewIdPB")]
SetLatestView = 221,
SetLatestView = 21,
/// Move the view or app to another place
#[event(input = "MoveFolderItemPayloadPB")]
MoveItem = 230,
MoveItem = 22,
/// Read the trash that was deleted by the user
#[event(output = "RepeatedTrashPB")]
ReadTrash = 300,
ReadTrash = 23,
/// Put back the trash to the origin folder
#[event(input = "TrashIdPB")]
PutbackTrash = 301,
PutbackTrash = 24,
/// Delete the trash from the disk
#[event(input = "RepeatedTrashIdPB")]
DeleteTrash = 302,
DeleteTrash = 25,
/// Put back all the trash to its original folder
#[event()]
RestoreAllTrash = 303,
RestoreAllTrash = 26,
/// Delete all the trash from the disk
#[event()]
DeleteAllTrash = 304,
DeleteAllTrash = 27,
#[event(input = "ImportPB")]
ImportData = 30,
}

View File

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

View File

@ -1,4 +1,5 @@
use std::collections::{HashMap, HashSet};
use std::ops::Deref;
use std::sync::Arc;
@ -12,7 +13,7 @@ use parking_lot::Mutex;
use tracing::{event, Level};
use crate::deps::{FolderCloudService, FolderUser};
use flowy_error::{FlowyError, FlowyResult};
use flowy_error::{ErrorCode, FlowyError, FlowyResult};
use lib_infra::util::timestamp;
use crate::entities::{
@ -23,16 +24,15 @@ use crate::notification::{
send_notification, send_workspace_notification, send_workspace_setting_notification,
FolderNotification,
};
use crate::share::ImportParams;
use crate::user_default::DefaultFolderBuilder;
use crate::view_ext::{
gen_view_id, view_from_create_view_params, ViewDataProcessor, ViewDataProcessorMap,
};
use crate::view_ext::{create_view, gen_view_id, FolderOperationHandler, FolderOperationHandlers};
pub struct Folder2Manager {
folder: Folder,
collab_builder: Arc<AppFlowyCollabBuilder>,
user: Arc<dyn FolderUser>,
view_processors: ViewDataProcessorMap,
operation_handlers: FolderOperationHandlers,
cloud_service: Arc<dyn FolderCloudService>,
}
@ -43,7 +43,7 @@ impl Folder2Manager {
pub async fn new(
user: Arc<dyn FolderUser>,
collab_builder: Arc<AppFlowyCollabBuilder>,
view_processors: ViewDataProcessorMap,
operation_handlers: FolderOperationHandlers,
cloud_service: Arc<dyn FolderCloudService>,
) -> FlowyResult<Self> {
let folder = Folder::default();
@ -51,7 +51,7 @@ impl Folder2Manager {
user,
folder,
collab_builder,
view_processors,
operation_handlers,
cloud_service,
};
@ -117,7 +117,7 @@ impl Folder2Manager {
let (folder_data, workspace_pb) = DefaultFolderBuilder::build(
self.user.user_id()?,
workspace_id.to_string(),
&self.view_processors,
&self.operation_handlers,
)
.await;
self.with_folder((), |folder| {
@ -187,14 +187,14 @@ impl Folder2Manager {
pub async fn create_view_with_params(&self, params: CreateViewParams) -> FlowyResult<View> {
let view_layout: ViewLayout = params.layout.clone().into();
let processor = self.get_data_processor(&view_layout)?;
let handler = self.get_handler(&view_layout)?;
let user_id = self.user.user_id()?;
let ext = params.ext.clone();
let ext = params.meta.clone();
match params.initial_data.is_empty() {
true => {
tracing::trace!("Create view with build-in data");
processor
.create_view_with_built_in_data(
handler
.create_built_in_view(
user_id,
&params.view_id,
&params.name,
@ -205,8 +205,8 @@ impl Folder2Manager {
},
false => {
tracing::trace!("Create view with view data");
processor
.create_view_with_custom_data(
handler
.create_view_with_view_data(
user_id,
&params.view_id,
&params.name,
@ -217,7 +217,7 @@ impl Folder2Manager {
.await?;
},
}
let view = view_from_create_view_params(params, view_layout);
let view = create_view(params, view_layout);
self.with_folder((), |folder| {
folder.insert_view(view.clone());
});
@ -233,12 +233,12 @@ impl Folder2Manager {
.ok_or_else(|| {
FlowyError::record_not_found().context("Can't find the view when closing the view")
})?;
let processor = self.get_data_processor(&view.layout)?;
processor.close_view(view_id).await?;
let handler = self.get_handler(&view.layout)?;
handler.close_view(view_id).await?;
Ok(())
}
pub async fn create_view_data(
pub async fn create_view_with_data(
&self,
view_id: &str,
name: &str,
@ -246,9 +246,9 @@ impl Folder2Manager {
data: Vec<u8>,
) -> FlowyResult<()> {
let user_id = self.user.user_id()?;
let processor = self.get_data_processor(&view_layout)?;
processor
.create_view_with_custom_data(
let handler = self.get_handler(&view_layout)?;
handler
.create_view_with_view_data(
user_id,
view_id,
name,
@ -367,20 +367,20 @@ impl Folder2Manager {
.with_folder(None, |folder| folder.views.get_view(view_id))
.ok_or_else(|| FlowyError::record_not_found().context("Can't duplicate the view"))?;
let processor = self.get_data_processor(&view.layout)?;
let view_data = processor.get_view_data(&view.id).await?;
let handler = self.get_handler(&view.layout)?;
let view_data = handler.duplicate_view(&view.id).await?;
let mut ext = HashMap::new();
if let Some(database_id) = view.database_id {
ext.insert("database_id".to_string(), database_id);
}
let duplicate_params = CreateViewParams {
belong_to_id: view.bid.clone(),
parent_view_id: view.bid.clone(),
name: format!("{} (copy)", &view.name),
desc: view.desc,
layout: view.layout.into(),
initial_data: view_data.to_vec(),
view_id: gen_view_id(),
ext,
meta: ext,
};
let _ = self.create_view_with_params(duplicate_params).await?;
@ -451,11 +451,52 @@ impl Folder2Manager {
.send();
}
fn get_data_processor(
pub(crate) async fn import(&self, import_data: ImportParams) -> FlowyResult<View> {
if import_data.data.is_none() && import_data.file_path.is_none() {
return Err(FlowyError::new(
ErrorCode::InvalidData,
"data or file_path is required",
));
}
let handler = self.get_handler(&import_data.view_layout)?;
let view_id = gen_view_id();
if let Some(data) = import_data.data {
handler
.import_from_bytes(&view_id, &import_data.name, data)
.await?;
}
if let Some(file_path) = import_data.file_path {
handler
.import_from_file_path(&view_id, &import_data.name, file_path)
.await?;
}
let params = CreateViewParams {
parent_view_id: import_data.parent_view_id,
name: import_data.name,
desc: "".to_string(),
layout: import_data.view_layout.clone().into(),
initial_data: vec![],
view_id,
meta: Default::default(),
};
let view = create_view(params, import_data.view_layout);
self.with_folder((), |folder| {
folder.insert_view(view.clone());
});
notify_parent_view_did_change(self.folder.clone(), vec![view.bid.clone()]);
Ok(view)
}
/// Returns a handler that implements the [FolderOperationHandler] trait
fn get_handler(
&self,
view_layout: &ViewLayout,
) -> FlowyResult<Arc<dyn ViewDataProcessor + Send + Sync>> {
match self.view_processors.get(view_layout) {
) -> FlowyResult<Arc<dyn FolderOperationHandler + Send + Sync>> {
match self.operation_handlers.get(view_layout) {
None => Err(FlowyError::internal().context(format!(
"Get data processor failed. Unknown layout type: {:?}",
view_layout

View File

@ -0,0 +1,17 @@
use collab_folder::core::ViewLayout;
#[derive(Clone, Debug)]
pub enum ImportType {
HistoryDocument = 0,
HistoryDatabase = 1,
}
#[derive(Clone, Debug)]
pub struct ImportParams {
pub parent_view_id: String,
pub name: String,
pub data: Option<Vec<u8>>,
pub file_path: Option<String>,
pub view_layout: ViewLayout,
pub import_type: ImportType,
}

View File

@ -0,0 +1,3 @@
mod import;
pub use import::*;

View File

@ -36,13 +36,13 @@ impl Folder2Manager {
) -> String {
let view_id = gen_view_id();
let params = CreateViewParams {
belong_to_id: app_id.to_string(),
parent_view_id: app_id.to_string(),
name: name.to_string(),
desc: "".to_string(),
layout,
view_id: view_id.clone(),
initial_data: vec![],
ext,
meta: ext,
};
self.create_view_with_params(params).await.unwrap();
view_id

View File

@ -5,14 +5,14 @@ use collab_folder::core::{Belonging, Belongings, FolderData, View, ViewLayout, W
use nanoid::nanoid;
use crate::entities::{view_pb_with_child_views, WorkspacePB};
use crate::view_ext::{gen_view_id, ViewDataProcessorMap};
use crate::view_ext::{gen_view_id, FolderOperationHandlers};
pub struct DefaultFolderBuilder();
impl DefaultFolderBuilder {
pub async fn build(
uid: i64,
workspace_id: String,
view_processors: &ViewDataProcessorMap,
handlers: &FolderOperationHandlers,
) -> (FolderData, WorkspacePB) {
let time = Utc::now().timestamp();
let view_id = gen_view_id();
@ -33,9 +33,9 @@ impl DefaultFolderBuilder {
// create the document
// TODO: use the initial data from the view processor
// let data = initial_read_me().into_bytes();
let processor = view_processors.get(&child_view_layout).unwrap();
processor
.create_view_with_built_in_data(
let handler = handlers.get(&child_view_layout).unwrap();
handler
.create_built_in_view(
uid,
&child_view.id,
&child_view.name,

View File

@ -8,29 +8,22 @@ use lib_infra::util::timestamp;
use std::collections::HashMap;
use std::sync::Arc;
pub trait ViewDataProcessor {
pub type ViewData = Bytes;
/// The handler will be used to handler the folder operation for a specific
/// view layout. Each [ViewLayout] will have a handler. So when creating a new
/// view, the [ViewLayout] will be used to get the handler.
///
pub trait FolderOperationHandler {
/// Closes the view and releases the resources that this view has in
/// the backend
fn close_view(&self, view_id: &str) -> FutureResult<(), FlowyError>;
/// Gets the data of the this view.
/// For example, the data can be used to duplicate the view.
fn get_view_data(&self, view_id: &str) -> FutureResult<Bytes, FlowyError>;
/// Create a view with the pre-defined data.
/// For example, the initial data of the grid/calendar/kanban board when
/// you create a new view.
fn create_view_with_built_in_data(
&self,
user_id: i64,
view_id: &str,
name: &str,
layout: ViewLayout,
ext: HashMap<String, String>,
) -> FutureResult<(), FlowyError>;
/// Returns the [ViewData] that can be used to create the same view.
fn duplicate_view(&self, view_id: &str) -> FutureResult<ViewData, FlowyError>;
/// Create a view with custom data
fn create_view_with_custom_data(
fn create_view_with_view_data(
&self,
user_id: i64,
view_id: &str,
@ -39,9 +32,38 @@ pub trait ViewDataProcessor {
layout: ViewLayout,
ext: HashMap<String, String>,
) -> FutureResult<(), FlowyError>;
/// Create a view with the pre-defined data.
/// For example, the initial data of the grid/calendar/kanban board when
/// you create a new view.
fn create_built_in_view(
&self,
user_id: i64,
view_id: &str,
name: &str,
layout: ViewLayout,
meta: HashMap<String, String>,
) -> FutureResult<(), FlowyError>;
/// Create a view by importing data
fn import_from_bytes(
&self,
view_id: &str,
name: &str,
bytes: Vec<u8>,
) -> FutureResult<(), FlowyError>;
/// Create a view by importing data from a file
fn import_from_file_path(
&self,
view_id: &str,
name: &str,
path: String,
) -> FutureResult<(), FlowyError>;
}
pub type ViewDataProcessorMap = Arc<HashMap<ViewLayout, Arc<dyn ViewDataProcessor + Send + Sync>>>;
pub type FolderOperationHandlers =
Arc<HashMap<ViewLayout, Arc<dyn FolderOperationHandler + Send + Sync>>>;
impl From<ViewLayoutPB> for ViewLayout {
fn from(pb: ViewLayoutPB) -> Self {
@ -54,11 +76,11 @@ impl From<ViewLayoutPB> for ViewLayout {
}
}
pub fn view_from_create_view_params(params: CreateViewParams, layout: ViewLayout) -> View {
pub(crate) fn create_view(params: CreateViewParams, layout: ViewLayout) -> View {
let time = timestamp();
View {
id: params.view_id,
bid: params.belong_to_id,
bid: params.parent_view_id,
name: params.name,
desc: params.desc,
belongings: Default::default(),

View File

@ -212,7 +212,7 @@ pub async fn read_workspace(sdk: &FlowyCoreTest, workspace_id: Option<String>) -
pub async fn create_app(sdk: &FlowyCoreTest, workspace_id: &str, name: &str, desc: &str) -> ViewPB {
let create_view_request = CreateViewPayloadPB {
belong_to_id: workspace_id.to_owned(),
parent_view_id: workspace_id.to_owned(),
name: name.to_string(),
desc: desc.to_string(),
thumbnail: None,
@ -237,7 +237,7 @@ pub async fn create_view(
layout: ViewLayout,
) -> ViewPB {
let request = CreateViewPayloadPB {
belong_to_id: app_id.to_string(),
parent_view_id: app_id.to_string(),
name: name.to_string(),
desc: desc.to_string(),
thumbnail: None,

View File

@ -69,7 +69,7 @@ async fn open_workspace(sdk: &FlowyCoreTest, workspace_id: &str) {
async fn create_app(sdk: &FlowyCoreTest, name: &str, desc: &str, workspace_id: &str) -> ViewPB {
let create_app_request = CreateViewPayloadPB {
belong_to_id: workspace_id.to_owned(),
parent_view_id: workspace_id.to_owned(),
name: name.to_string(),
desc: desc.to_string(),
thumbnail: None,
@ -93,7 +93,7 @@ async fn create_view(
data: Vec<u8>,
) -> ViewPB {
let payload = CreateViewPayloadPB {
belong_to_id: app_id.to_string(),
parent_view_id: app_id.to_string(),
name: "View A".to_string(),
desc: "".to_string(),
thumbnail: Some("http://1.png".to_string()),