diff --git a/frontend/appflowy_flutter/integration_test/desktop/uncategorized/share_markdown_test.dart b/frontend/appflowy_flutter/integration_test/desktop/uncategorized/share_markdown_test.dart index 26788beb9cd6f..9a4fe30815b1e 100644 --- a/frontend/appflowy_flutter/integration_test/desktop/uncategorized/share_markdown_test.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/uncategorized/share_markdown_test.dart @@ -1,6 +1,6 @@ import 'dart:io'; -import 'package:appflowy/plugins/document/presentation/share/share_button.dart'; +import 'package:appflowy/plugins/shared/share/share_button.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; import 'package:path/path.dart' as p; @@ -51,9 +51,8 @@ void main() { }, ); - final shareButton = find.byType(DocumentShareButton); - final shareButtonState = - tester.widget(shareButton) as DocumentShareButton; + final shareButton = find.byType(ShareButton); + final shareButtonState = tester.widget(shareButton) as ShareButton; final path = await mockSaveFilePath( p.join( diff --git a/frontend/appflowy_flutter/integration_test/shared/common_operations.dart b/frontend/appflowy_flutter/integration_test/shared/common_operations.dart index 6d7cf5ab2fe42..9192ee3cf872e 100644 --- a/frontend/appflowy_flutter/integration_test/shared/common_operations.dart +++ b/frontend/appflowy_flutter/integration_test/shared/common_operations.dart @@ -1,17 +1,12 @@ import 'dart:io'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/gestures.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; - import 'package:appflowy/core/config/kv.dart'; import 'package:appflowy/core/config/kv_keys.dart'; import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/mobile/presentation/presentation.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/base/emoji_picker_button.dart'; -import 'package:appflowy/plugins/document/presentation/share/share_button.dart'; +import 'package:appflowy/plugins/shared/share/share_button.dart'; import 'package:appflowy/shared/feature_flags.dart'; import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/user/presentation/screens/screens.dart'; @@ -35,6 +30,10 @@ import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/widget/buttons/primary_button.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'emoji.dart'; @@ -259,7 +258,7 @@ extension CommonOperations on WidgetTester { /// Tap the share button above the document page. Future tapShareButton() async { final shareButton = find.byWidgetPredicate( - (widget) => widget is DocumentShareButton, + (widget) => widget is ShareButton, ); await tapButton(shareButton); } diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/chat.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/chat.dart index c34b0abcfe099..d5c76be5175aa 100644 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/chat.dart +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/chat.dart @@ -1,5 +1,3 @@ -import 'package:flutter/material.dart'; - import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/plugins/ai_chat/chat_page.dart'; import 'package:appflowy/plugins/util.dart'; @@ -9,6 +7,7 @@ import 'package:appflowy/workspace/presentation/home/home_stack.dart'; import 'package:appflowy/workspace/presentation/widgets/tab_bar_item.dart'; import 'package:appflowy/workspace/presentation/widgets/view_title_bar.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; class AIChatPluginBuilder extends PluginBuilder { @@ -96,6 +95,7 @@ class AIChatPagePluginWidgetBuilder extends PluginWidgetBuilder Widget buildWidget({ required PluginContext context, required bool shrinkWrap, + Map? data, }) { notifier.isDeleted.addListener(() { final deletedView = notifier.isDeleted.value; diff --git a/frontend/appflowy_flutter/lib/plugins/blank/blank.dart b/frontend/appflowy_flutter/lib/plugins/blank/blank.dart index 1aec6b7037f6a..a74f8d7ee06af 100644 --- a/frontend/appflowy_flutter/lib/plugins/blank/blank.dart +++ b/frontend/appflowy_flutter/lib/plugins/blank/blank.dart @@ -54,6 +54,7 @@ class BlankPagePluginWidgetBuilder extends PluginWidgetBuilder Widget buildWidget({ required PluginContext context, required bool shrinkWrap, + Map? data, }) => const BlankPage(); diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/grid_page.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/grid_page.dart index 67e238ed51bfb..50b67e7a8faa2 100755 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/grid_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/grid_page.dart @@ -1,6 +1,3 @@ -import 'package:appflowy/workspace/application/view/view_bloc.dart'; -import 'package:flutter/material.dart'; - import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/database/application/row/row_service.dart'; import 'package:appflowy/plugins/database/grid/presentation/widgets/calculations/calculations_row.dart'; @@ -9,6 +6,7 @@ import 'package:appflowy/plugins/database/tab_bar/desktop/setting_menu.dart'; import 'package:appflowy/plugins/database/widgets/cell/editable_cell_builder.dart'; import 'package:appflowy/workspace/application/action_navigation/action_navigation_bloc.dart'; import 'package:appflowy/workspace/application/action_navigation/navigation_action.dart'; +import 'package:appflowy/workspace/application/view/view_bloc.dart'; import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:collection/collection.dart'; @@ -16,6 +14,7 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/style_widget/scrolling/styled_scrollview.dart'; import 'package:flowy_infra_ui/widget/error_page.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:linked_scroll_controller/linked_scroll_controller.dart'; @@ -24,7 +23,6 @@ import '../../application/row/row_controller.dart'; import '../../tab_bar/tab_bar_view.dart'; import '../../widgets/row/row_detail.dart'; import '../application/grid_bloc.dart'; - import 'grid_scroll.dart'; import 'layout/layout.dart'; import 'layout/sizes.dart'; @@ -504,7 +502,10 @@ class _PositionedCalculationsRowState left: 0, right: 0, child: Container( - margin: EdgeInsets.only(left: GridSize.horizontalHeaderPadding + 40), + margin: EdgeInsets.only( + left: + context.read().horizontalPadding, + ), padding: const EdgeInsets.only(bottom: 10), decoration: BoxDecoration( color: Theme.of(context).canvasColor, diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/header/grid_header.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/header/grid_header.dart index 3b6b4320d7634..baa62657b82f1 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/header/grid_header.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/header/grid_header.dart @@ -4,6 +4,7 @@ import 'package:appflowy/plugins/database/application/field/field_controller.dar import 'package:appflowy/plugins/database/domain/field_service.dart'; import 'package:appflowy/plugins/database/grid/application/grid_bloc.dart'; import 'package:appflowy/plugins/database/grid/application/grid_header_bloc.dart'; +import 'package:appflowy/plugins/database/tab_bar/tab_bar_view.dart'; import 'package:appflowy_backend/log.dart'; import 'package:appflowy_editor/appflowy_editor.dart' hide Log; import 'package:easy_localization/easy_localization.dart'; @@ -139,7 +140,9 @@ class _GridHeaderState extends State<_GridHeader> { } Widget _cellLeading() { - return SizedBox(width: GridSize.horizontalHeaderPadding + 40); + return SizedBox( + width: context.read().horizontalPadding, + ); } } diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/row/row.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/row/row.dart index 351062933aac3..337aab4a705f3 100755 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/row/row.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/row/row.dart @@ -1,5 +1,3 @@ -import 'package:flutter/material.dart'; - import 'package:appflowy/generated/flowy_svgs.g.dart'; import "package:appflowy/generated/locale_keys.g.dart"; import 'package:appflowy/plugins/database/application/cell/cell_controller.dart'; @@ -7,18 +5,19 @@ import 'package:appflowy/plugins/database/application/field/field_controller.dar import 'package:appflowy/plugins/database/application/row/row_controller.dart'; import 'package:appflowy/plugins/database/application/row/row_service.dart'; import 'package:appflowy/plugins/database/grid/application/row/row_bloc.dart'; +import 'package:appflowy/plugins/database/tab_bar/tab_bar_view.dart'; import 'package:appflowy/plugins/database/widgets/cell/editable_cell_builder.dart'; import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra/theme_extension.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:provider/provider.dart'; import '../../../../widgets/row/accessory/cell_accessory.dart'; import '../../../../widgets/row/cells/cell_container.dart'; import '../../layout/sizes.dart'; - import 'action.dart'; class GridRow extends StatefulWidget { @@ -112,7 +111,9 @@ class _RowLeadingState extends State<_RowLeading> { child: Consumer( builder: (context, state, _) { return SizedBox( - width: GridSize.horizontalHeaderPadding + 40, + width: context + .read() + .horizontalPadding, child: state.onEnter ? _activeWidget() : null, ); }, diff --git a/frontend/appflowy_flutter/lib/plugins/database/tab_bar/desktop/tab_bar_header.dart b/frontend/appflowy_flutter/lib/plugins/database/tab_bar/desktop/tab_bar_header.dart index 54a08c128461d..9c853414f15ab 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/tab_bar/desktop/tab_bar_header.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/tab_bar/desktop/tab_bar_header.dart @@ -1,7 +1,7 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/database/application/tab_bar_bloc.dart'; -import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart'; +import 'package:appflowy/plugins/database/tab_bar/tab_bar_view.dart'; import 'package:appflowy/workspace/application/view/view_ext.dart'; import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart'; @@ -17,14 +17,17 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'tab_bar_add_button.dart'; class TabBarHeader extends StatelessWidget { - const TabBarHeader({super.key}); + const TabBarHeader({ + super.key, + }); @override Widget build(BuildContext context) { return Container( height: 30, padding: EdgeInsets.symmetric( - horizontal: GridSize.horizontalHeaderPadding + 40, + horizontal: + context.read().horizontalPadding, ), child: Stack( children: [ diff --git a/frontend/appflowy_flutter/lib/plugins/database/tab_bar/tab_bar_view.dart b/frontend/appflowy_flutter/lib/plugins/database/tab_bar/tab_bar_view.dart index 690bac3fab5bb..0f8f84174fc83 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/tab_bar/tab_bar_view.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/tab_bar/tab_bar_view.dart @@ -1,6 +1,7 @@ import 'package:appflowy/plugins/database/application/database_controller.dart'; import 'package:appflowy/plugins/database/application/tab_bar_bloc.dart'; -import 'package:appflowy/plugins/database/widgets/share_button.dart'; +import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart'; +import 'package:appflowy/plugins/shared/share/share_button.dart'; import 'package:appflowy/plugins/util.dart'; import 'package:appflowy/startup/plugin/plugin.dart'; import 'package:appflowy/workspace/application/view/view_bloc.dart'; @@ -16,6 +17,7 @@ import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/widget/spacing.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:provider/provider.dart'; import 'desktop/tab_bar_header.dart'; import 'mobile/mobile_tab_bar_header.dart'; @@ -219,6 +221,16 @@ class DatabaseTabBarViewPlugin extends Plugin { } } +const kDatabasePluginWidgetBuilderHorizontalPadding = 'horizontal_padding'; + +class DatabasePluginWidgetBuilderSize { + const DatabasePluginWidgetBuilderSize({ + required this.horizontalPadding, + }); + + final double horizontalPadding; +} + class DatabasePluginWidgetBuilder extends PluginWidgetBuilder { DatabasePluginWidgetBuilder({ required this.bloc, @@ -244,6 +256,7 @@ class DatabasePluginWidgetBuilder extends PluginWidgetBuilder { Widget buildWidget({ required PluginContext context, required bool shrinkWrap, + Map? data, }) { notifier.isDeleted.addListener(() { final deletedView = notifier.isDeleted.value; @@ -252,11 +265,20 @@ class DatabasePluginWidgetBuilder extends PluginWidgetBuilder { } }); - return DatabaseTabBarView( - key: ValueKey(notifier.view.id), - view: notifier.view, - shrinkWrap: shrinkWrap, - initialRowId: initialRowId, + final horizontalPadding = + data?[kDatabasePluginWidgetBuilderHorizontalPadding] as double? ?? + GridSize.horizontalHeaderPadding + 40; + + return Provider( + create: (context) => DatabasePluginWidgetBuilderSize( + horizontalPadding: horizontalPadding, + ), + child: DatabaseTabBarView( + key: ValueKey(notifier.view.id), + view: notifier.view, + shrinkWrap: shrinkWrap, + initialRowId: initialRowId, + ), ); } @@ -270,7 +292,7 @@ class DatabasePluginWidgetBuilder extends PluginWidgetBuilder { value: bloc, child: Row( children: [ - DatabaseShareButton(key: ValueKey(view.id), view: view), + ShareButton(key: ValueKey(view.id), view: view), const HSpace(10), ViewFavoriteButton(view: view), const HSpace(4), diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/database_view_widget.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/database_view_widget.dart index ee64eb84af439..35169550f1c15 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/database_view_widget.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/database_view_widget.dart @@ -1,9 +1,9 @@ -import 'package:flutter/material.dart'; - +import 'package:appflowy/plugins/database/tab_bar/tab_bar_view.dart'; import 'package:appflowy/startup/plugin/plugin.dart'; import 'package:appflowy/workspace/application/view/view_ext.dart'; import 'package:appflowy/workspace/application/view/view_listener.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; +import 'package:flutter/material.dart'; class DatabaseViewWidget extends StatefulWidget { const DatabaseViewWidget({ @@ -55,6 +55,9 @@ class _DatabaseViewWidgetState extends State { builder: (_, __, ___) => viewPlugin.widgetBuilder.buildWidget( shrinkWrap: widget.shrinkWrap, context: PluginContext(), + data: { + kDatabasePluginWidgetBuilderHorizontalPadding: 40.0, + }, ), ); } diff --git a/frontend/appflowy_flutter/lib/plugins/database_document/database_document_plugin.dart b/frontend/appflowy_flutter/lib/plugins/database_document/database_document_plugin.dart index e415aaa12e317..3e5b40396a2c1 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_document/database_document_plugin.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_document/database_document_plugin.dart @@ -104,6 +104,7 @@ class DatabaseDocumentPluginWidgetBuilder extends PluginWidgetBuilder Widget buildWidget({ required PluginContext context, required bool shrinkWrap, + Map? data, }) { return BlocBuilder( builder: (_, state) => DatabaseDocumentPage( diff --git a/frontend/appflowy_flutter/lib/plugins/document/application/prelude.dart b/frontend/appflowy_flutter/lib/plugins/document/application/prelude.dart index 78f30608d7911..a6497bf6de48f 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/application/prelude.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/application/prelude.dart @@ -1,3 +1,3 @@ +export '../../shared/share/share_bloc.dart'; export 'document_bloc.dart'; export 'document_service.dart'; -export 'document_share_bloc.dart'; diff --git a/frontend/appflowy_flutter/lib/plugins/document/document.dart b/frontend/appflowy_flutter/lib/plugins/document/document.dart index ef11ef3403b01..aa0154bf92cb0 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/document.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/document.dart @@ -5,7 +5,7 @@ import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/document/application/document_appearance_cubit.dart'; import 'package:appflowy/plugins/document/document_page.dart'; import 'package:appflowy/plugins/document/presentation/document_collaborators.dart'; -import 'package:appflowy/plugins/document/presentation/share/share_button.dart'; +import 'package:appflowy/plugins/shared/share/share_button.dart'; import 'package:appflowy/plugins/util.dart'; import 'package:appflowy/shared/feature_flags.dart'; import 'package:appflowy/startup/plugin/plugin.dart'; @@ -109,6 +109,7 @@ class DocumentPluginWidgetBuilder extends PluginWidgetBuilder Widget buildWidget({ required PluginContext context, required bool shrinkWrap, + Map? data, }) { notifier.isDeleted.addListener(() { final deletedView = notifier.isDeleted.value; @@ -154,7 +155,7 @@ class DocumentPluginWidgetBuilder extends PluginWidgetBuilder const HSpace(16), ] : [const HSpace(8)], - DocumentShareButton( + ShareButton( key: ValueKey('share_button_${view.id}'), view: view, ), diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/built_in_page_widget.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/built_in_page_widget.dart index d7e819486715a..1a0d9418efc31 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/built_in_page_widget.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/built_in_page_widget.dart @@ -13,9 +13,7 @@ import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:appflowy_result/appflowy_result.dart'; import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra_ui/style_widget/icon_button.dart'; -import 'package:flowy_infra_ui/style_widget/text.dart'; -import 'package:flowy_infra_ui/widget/spacing.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; class BuiltInPageWidget extends StatefulWidget { @@ -127,7 +125,6 @@ class _BuiltInPageWidgetState extends State { icon: const FlowySvg( FlowySvgs.information_s, ), - iconColorOnHover: Theme.of(context).colorScheme.onSecondary, ), // setting const Space(7, 0), @@ -141,7 +138,6 @@ class _BuiltInPageWidgetState extends State { width: 24, height: 24, iconPadding: const EdgeInsets.all(3), - iconColorOnHover: Theme.of(context).colorScheme.onSecondary, icon: const FlowySvg( FlowySvgs.settings_s, ), diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/share/publish_tab.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/share/publish_tab.dart deleted file mode 100644 index f5e52b8958b48..0000000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/share/publish_tab.dart +++ /dev/null @@ -1,284 +0,0 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/document/application/document_share_bloc.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart'; -import 'package:appflowy/plugins/document/presentation/share/pubish_color_extension.dart'; -import 'package:appflowy/plugins/document/presentation/share/publish_name_generator.dart'; -import 'package:appflowy/startup/startup.dart'; -import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flowy_infra_ui/style_widget/hover.dart'; -import 'package:flowy_infra_ui/widget/rounded_button.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -class PublishTab extends StatelessWidget { - const PublishTab({super.key}); - - @override - Widget build(BuildContext context) { - return BlocConsumer( - listener: (context, state) { - _showToast(context, state); - }, - builder: (context, state) { - return state.isPublished - ? _PublishedWidget( - url: state.url, - onVisitSite: () {}, - onUnPublish: () { - context - .read() - .add(const DocumentShareEvent.unPublish()); - }, - ) - : _UnPublishWidget( - onPublish: () async { - final id = context.read().view.id; - final publishName = await generatePublishName( - id, - state.viewName, - ); - if (context.mounted) { - context.read().add( - DocumentShareEvent.publish('', publishName), - ); - } - }, - ); - }, - ); - } - - void _showToast(BuildContext context, DocumentShareState state) { - if (state.publishResult != null) { - state.publishResult!.fold( - (value) => showToastNotification( - context, - message: LocaleKeys.publish_publishSuccessfully.tr(), - ), - (error) => showToastNotification( - context, - message: '${LocaleKeys.publish_publishFailed.tr()}: ${error.code}', - ), - ); - } else if (state.unpublishResult != null) { - state.unpublishResult!.fold( - (value) => showToastNotification( - context, - message: LocaleKeys.publish_unpublishSuccessfully.tr(), - ), - (error) => showToastNotification( - context, - message: LocaleKeys.publish_unpublishFailed.tr(), - description: error.msg, - ), - ); - } - } -} - -class _PublishedWidget extends StatefulWidget { - const _PublishedWidget({ - required this.url, - required this.onVisitSite, - required this.onUnPublish, - }); - - final String url; - final VoidCallback onVisitSite; - final VoidCallback onUnPublish; - - @override - State<_PublishedWidget> createState() => _PublishedWidgetState(); -} - -class _PublishedWidgetState extends State<_PublishedWidget> { - final controller = TextEditingController(); - - @override - void initState() { - super.initState(); - controller.text = widget.url; - } - - @override - void dispose() { - controller.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const VSpace(16), - const _PublishTabHeader(), - const VSpace(16), - _PublishUrl( - controller: controller, - onCopy: (url) { - getIt().setData( - ClipboardServiceData(plainText: url), - ); - - showToastNotification( - context, - message: LocaleKeys.grid_url_copy.tr(), - ); - }, - onSubmitted: (url) {}, - ), - const VSpace(16), - Row( - mainAxisSize: MainAxisSize.min, - children: [ - _buildUnpublishButton(), - const Spacer(), - _buildVisitSiteButton(), - ], - ), - ], - ); - } - - Widget _buildUnpublishButton() { - return SizedBox( - height: 36, - width: 184, - child: FlowyButton( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(10), - border: Border.all(color: ShareMenuColors.borderColor(context)), - ), - radius: BorderRadius.circular(10), - text: FlowyText.regular( - LocaleKeys.shareAction_unPublish.tr(), - textAlign: TextAlign.center, - ), - onTap: widget.onUnPublish, - ), - ); - } - - Widget _buildVisitSiteButton() { - return RoundedTextButton( - onPressed: () { - safeLaunchUrl(controller.text); - }, - title: LocaleKeys.shareAction_visitSite.tr(), - width: 184, - height: 36, - borderRadius: const BorderRadius.all(Radius.circular(10)), - fillColor: Theme.of(context).colorScheme.primary, - textColor: Theme.of(context).colorScheme.onPrimary, - ); - } -} - -class _UnPublishWidget extends StatelessWidget { - const _UnPublishWidget({ - required this.onPublish, - }); - - final VoidCallback onPublish; - - @override - Widget build(BuildContext context) { - return Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const VSpace(16), - const _PublishTabHeader(), - const VSpace(16), - RoundedTextButton( - height: 36, - title: LocaleKeys.shareAction_publish.tr(), - padding: const EdgeInsets.symmetric(vertical: 9.0), - fontSize: 14.0, - textColor: Theme.of(context).colorScheme.onPrimary, - onPressed: onPublish, - ), - ], - ); - } -} - -class _PublishTabHeader extends StatelessWidget { - const _PublishTabHeader(); - - @override - Widget build(BuildContext context) { - return Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - const FlowySvg(FlowySvgs.share_publish_s), - const HSpace(6), - FlowyText(LocaleKeys.shareAction_publishToTheWeb.tr()), - ], - ), - const VSpace(4), - FlowyText.regular( - LocaleKeys.shareAction_publishToTheWebHint.tr(), - fontSize: 12, - maxLines: 3, - color: Theme.of(context).hintColor, - ), - ], - ); - } -} - -class _PublishUrl extends StatelessWidget { - const _PublishUrl({ - required this.controller, - required this.onCopy, - required this.onSubmitted, - }); - - final TextEditingController controller; - final void Function(String url) onCopy; - final void Function(String url) onSubmitted; - - @override - Widget build(BuildContext context) { - return SizedBox( - height: 36, - child: FlowyTextField( - readOnly: true, - autoFocus: false, - controller: controller, - enableBorderColor: ShareMenuColors.borderColor(context), - suffixIcon: _buildCopyLinkIcon(context), - ), - ); - } - - Widget _buildCopyLinkIcon(BuildContext context) { - return FlowyHover( - child: GestureDetector( - onTap: () => onCopy(controller.text), - child: Container( - width: 36, - height: 36, - alignment: Alignment.center, - padding: const EdgeInsets.all(10), - decoration: const BoxDecoration( - border: Border(left: BorderSide(color: Color(0x141F2329))), - ), - child: const FlowySvg( - FlowySvgs.m_toolbar_link_m, - ), - ), - ), - ); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/share/share_button.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/share/share_button.dart deleted file mode 100644 index 0770cc98fcb52..0000000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/share/share_button.dart +++ /dev/null @@ -1,248 +0,0 @@ -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/document/application/document_share_bloc.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart'; -import 'package:appflowy/plugins/document/presentation/share/share_menu.dart'; -import 'package:appflowy/startup/startup.dart'; -import 'package:appflowy/util/string_extension.dart'; -import 'package:appflowy/workspace/application/export/document_exporter.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_backend/protobuf/flowy-document/entities.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.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/flowy_infra_ui.dart'; -import 'package:flowy_infra_ui/widget/rounded_button.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -class DocumentShareButton extends StatelessWidget { - const DocumentShareButton({ - super.key, - required this.view, - }); - - final ViewPB view; - - @override - Widget build(BuildContext context) { - return BlocProvider( - create: (context) => getIt(param1: view) - ..add(const DocumentShareEvent.initial()), - child: BlocListener( - listener: (context, state) { - if (state.isLoading == false && state.exportResult != null) { - state.exportResult!.fold( - (data) => _handleExportData(context, data), - _handleExportError, - ); - } - }, - child: BlocBuilder( - builder: (context, state) { - final tabs = [ - if (state.enablePublish) ShareMenuTab.publish, - ShareMenuTab.exportAs, - ]; - final shareBloc = context.read(); - return SizedBox( - height: 32.0, - child: IntrinsicWidth( - child: AppFlowyPopover( - direction: PopoverDirection.bottomWithRightAligned, - constraints: const BoxConstraints( - maxWidth: 422, - ), - offset: const Offset(0, 8), - popupBuilder: (context) => BlocProvider.value( - value: shareBloc, - child: ShareMenu( - tabs: tabs, - ), - ), - child: const InnerDocumentShareButton(), - ), - ), - ); - }, - ), - ), - ); - } - - void _handleExportData(BuildContext context, ExportDataPB exportData) { - switch (exportData.exportType) { - case ExportType.Markdown: - showSnackBarMessage( - context, - LocaleKeys.settings_files_exportFileSuccess.tr(), - ); - break; - case ExportType.Link: - case ExportType.Text: - break; - case ExportType.HTML: - showSnackBarMessage( - context, - LocaleKeys.settings_files_exportFileSuccess.tr(), - ); - break; - } - } - - void _handleExportError(FlowyError error) { - showMessageToast(error.msg); - } -} - -class InnerDocumentShareButton extends StatelessWidget { - const InnerDocumentShareButton({super.key}); - - @override - Widget build(BuildContext context) { - return RoundedTextButton( - title: LocaleKeys.shareAction_buttonText.tr(), - padding: const EdgeInsets.symmetric(horizontal: 14.0), - fontSize: 14.0, - fontWeight: FontWeight.w500, - borderRadius: const BorderRadius.all( - Radius.circular(10.0), - ), - textColor: Theme.of(context).colorScheme.onPrimary, - ); - } -} - -class ShareActionList extends StatefulWidget { - const ShareActionList({ - super.key, - required this.view, - }); - - final ViewPB view; - - @override - State createState() => ShareActionListState(); -} - -@visibleForTesting -class ShareActionListState 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 docShareBloc = context.read(); - return PopoverActionList( - direction: PopoverDirection.bottomWithCenterAligned, - offset: const Offset(0, 8), - actions: ShareAction.values - .map((action) => ShareActionWrapper(action)) - .toList(), - buildChild: (controller) => Listener( - onPointerDown: (_) => controller.show(), - child: RoundedTextButton( - title: LocaleKeys.shareAction_buttonText.tr(), - padding: const EdgeInsets.symmetric(horizontal: 12.0), - onPressed: () {}, - fontSize: 14.0, - textColor: Theme.of(context).colorScheme.onPrimary, - ), - ), - onSelected: (action, controller) async { - switch (action.inner) { - case ShareAction.markdown: - final exportPath = await getIt().saveFile( - dialogTitle: '', - // encode the file name in case it contains special characters - fileName: '${name.toFileName()}.md', - ); - if (exportPath != null) { - docShareBloc.add( - DocumentShareEvent.share( - DocumentShareType.markdown, - exportPath, - ), - ); - } - break; - case ShareAction.html: - final exportPath = await getIt().saveFile( - dialogTitle: '', - fileName: '${name.toFileName()}.html', - ); - if (exportPath != null) { - docShareBloc.add( - DocumentShareEvent.share( - DocumentShareType.html, - exportPath, - ), - ); - } - break; - case ShareAction.clipboard: - final documentExporter = DocumentExporter(widget.view); - final result = - await documentExporter.export(DocumentExportType.markdown); - result.fold( - (markdown) => getIt() - .setData(ClipboardServiceData(plainText: markdown)), - (error) => showMessageToast(error.msg), - ); - break; - } - controller.close(); - }, - ); - } - - void listenOnViewUpdated() { - name = widget.view.name; - viewListener.start( - onViewUpdated: (view) { - name = view.name; - }, - ); - } -} - -enum ShareAction { - markdown, - html, - clipboard, -} - -class ShareActionWrapper extends ActionCell { - ShareActionWrapper(this.inner); - - final ShareAction inner; - - Widget? icon(Color iconColor) => null; - - @override - String get name { - switch (inner) { - case ShareAction.markdown: - return LocaleKeys.shareAction_markdown.tr(); - case ShareAction.html: - return LocaleKeys.shareAction_html.tr(); - case ShareAction.clipboard: - return LocaleKeys.shareAction_clipboard.tr(); - } - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/shared/share/_shared.dart b/frontend/appflowy_flutter/lib/plugins/shared/share/_shared.dart new file mode 100644 index 0000000000000..391b0836d75d2 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/shared/share/_shared.dart @@ -0,0 +1,68 @@ +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/plugins/database/application/tab_bar_bloc.dart'; +import 'package:appflowy/plugins/shared/share/share_bloc.dart'; +import 'package:appflowy/plugins/shared/share/share_menu.dart'; +import 'package:appflowy_popover/appflowy_popover.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flowy_infra_ui/widget/rounded_button.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class ShareMenuButton extends StatelessWidget { + const ShareMenuButton({ + super.key, + required this.tabs, + }); + + final List tabs; + + @override + Widget build(BuildContext context) { + final shareBloc = context.read(); + final databaseBloc = context.read(); + return SizedBox( + height: 32.0, + child: IntrinsicWidth( + child: AppFlowyPopover( + direction: PopoverDirection.bottomWithRightAligned, + constraints: const BoxConstraints( + maxWidth: 422, + ), + offset: const Offset(0, 8), + popupBuilder: (context) => MultiBlocProvider( + providers: [ + if (databaseBloc != null) + BlocProvider.value( + value: databaseBloc, + ), + BlocProvider.value(value: shareBloc), + ], + child: ShareMenu( + tabs: tabs, + ), + ), + child: const _ShareButton(), + ), + ), + ); + } +} + +class _ShareButton extends StatelessWidget { + const _ShareButton(); + + @override + Widget build(BuildContext context) { + return RoundedTextButton( + title: LocaleKeys.shareAction_buttonText.tr(), + padding: const EdgeInsets.symmetric(horizontal: 14.0), + fontSize: 14.0, + fontWeight: FontWeight.w500, + borderRadius: const BorderRadius.all( + Radius.circular(10.0), + ), + textColor: Theme.of(context).colorScheme.onPrimary, + ); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/share/export_tab.dart b/frontend/appflowy_flutter/lib/plugins/shared/share/export_tab.dart similarity index 69% rename from frontend/appflowy_flutter/lib/plugins/document/presentation/share/export_tab.dart rename to frontend/appflowy_flutter/lib/plugins/shared/share/export_tab.dart index c5bbecbe0b450..223db0e0e11ee 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/share/export_tab.dart +++ b/frontend/appflowy_flutter/lib/plugins/shared/share/export_tab.dart @@ -1,12 +1,13 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/document/application/document_share_bloc.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart'; +import 'package:appflowy/plugins/shared/share/share_bloc.dart'; import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/util/string_extension.dart'; import 'package:appflowy/util/theme_extension.dart'; import 'package:appflowy/workspace/application/export/document_exporter.dart'; import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra/file_picker/file_picker_service.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; @@ -20,6 +21,16 @@ class ExportTab extends StatelessWidget { @override Widget build(BuildContext context) { + final view = context.read().view; + + if (view.layout == ViewLayoutPB.Document) { + return _buildDocumentExportTab(context); + } + + return _buildDatabaseExportTab(context); + } + + Widget _buildDocumentExportTab(BuildContext context) { return Column( children: [ const VSpace(10), @@ -44,16 +55,29 @@ class ExportTab extends StatelessWidget { ); } + Widget _buildDatabaseExportTab(BuildContext context) { + return Column( + children: [ + const VSpace(10), + _ExportButton( + title: LocaleKeys.shareAction_csv.tr(), + svg: FlowySvgs.database_layout_m, + onTap: () => _exportCSV(context), + ), + ], + ); + } + Future _exportHTML(BuildContext context) async { - final viewName = context.read().state.viewName; + final viewName = context.read().state.viewName; final exportPath = await getIt().saveFile( dialogTitle: '', fileName: '${viewName.toFileName()}.html', ); if (context.mounted && exportPath != null) { - context.read().add( - DocumentShareEvent.share( - DocumentShareType.html, + context.read().add( + ShareEvent.share( + ShareType.html, exportPath, ), ); @@ -61,15 +85,31 @@ class ExportTab extends StatelessWidget { } Future _exportMarkdown(BuildContext context) async { - final viewName = context.read().state.viewName; + final viewName = context.read().state.viewName; final exportPath = await getIt().saveFile( dialogTitle: '', fileName: '${viewName.toFileName()}.md', ); if (context.mounted && exportPath != null) { - context.read().add( - DocumentShareEvent.share( - DocumentShareType.markdown, + context.read().add( + ShareEvent.share( + ShareType.markdown, + exportPath, + ), + ); + } + } + + Future _exportCSV(BuildContext context) async { + final viewName = context.read().state.viewName; + final exportPath = await getIt().saveFile( + dialogTitle: '', + fileName: '${viewName.toFileName()}.csv', + ); + if (context.mounted && exportPath != null) { + context.read().add( + ShareEvent.share( + ShareType.csv, exportPath, ), ); @@ -77,8 +117,7 @@ class ExportTab extends StatelessWidget { } Future _exportToClipboard(BuildContext context) async { - final documentExporter = - DocumentExporter(context.read().view); + final documentExporter = DocumentExporter(context.read().view); final result = await documentExporter.export(DocumentExportType.markdown); result.fold( (markdown) { diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/share/pubish_color_extension.dart b/frontend/appflowy_flutter/lib/plugins/shared/share/publish_color_extension.dart similarity index 100% rename from frontend/appflowy_flutter/lib/plugins/document/presentation/share/pubish_color_extension.dart rename to frontend/appflowy_flutter/lib/plugins/shared/share/publish_color_extension.dart diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/share/publish_name_generator.dart b/frontend/appflowy_flutter/lib/plugins/shared/share/publish_name_generator.dart similarity index 100% rename from frontend/appflowy_flutter/lib/plugins/document/presentation/share/publish_name_generator.dart rename to frontend/appflowy_flutter/lib/plugins/shared/share/publish_name_generator.dart diff --git a/frontend/appflowy_flutter/lib/plugins/shared/share/publish_tab.dart b/frontend/appflowy_flutter/lib/plugins/shared/share/publish_tab.dart new file mode 100644 index 0000000000000..a1baf32e30ba6 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/shared/share/publish_tab.dart @@ -0,0 +1,559 @@ +import 'package:appflowy/core/helpers/url_launcher.dart'; +import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/plugins/database/application/tab_bar_bloc.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart'; +import 'package:appflowy/plugins/shared/share/publish_color_extension.dart'; +import 'package:appflowy/plugins/shared/share/publish_name_generator.dart'; +import 'package:appflowy/plugins/shared/share/share_bloc.dart'; +import 'package:appflowy/startup/startup.dart'; +import 'package:appflowy/workspace/application/view/view_ext.dart'; +import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; +import 'package:appflowy_backend/log.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; +import 'package:appflowy_editor/appflowy_editor.dart' hide Log; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flowy_infra_ui/style_widget/hover.dart'; +import 'package:flowy_infra_ui/widget/flowy_tooltip.dart'; +import 'package:flowy_infra_ui/widget/rounded_button.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class PublishTab extends StatelessWidget { + const PublishTab({super.key}); + + @override + Widget build(BuildContext context) { + return BlocConsumer( + listener: (context, state) { + _showToast(context, state); + }, + builder: (context, state) { + if (state.isPublished) { + return _PublishedWidget( + url: state.url, + onVisitSite: (url) => afLaunchUrlString(url), + onUnPublish: () { + context.read().add(const ShareEvent.unPublish()); + }, + ); + } else { + return _PublishWidget( + onPublish: (selectedViews) async { + final id = context.read().view.id; + final publishName = await generatePublishName( + id, + state.viewName, + ); + + if (selectedViews.isNotEmpty) { + Log.info( + 'Publishing views: ${selectedViews.map((e) => e.name)}', + ); + } + + if (context.mounted) { + context.read().add( + ShareEvent.publish( + '', + publishName, + selectedViews.map((e) => e.id).toList(), + ), + ); + } + }, + ); + } + }, + ); + } + + void _showToast(BuildContext context, ShareState state) { + if (state.publishResult != null) { + state.publishResult!.fold( + (value) => showToastNotification( + context, + message: LocaleKeys.publish_publishSuccessfully.tr(), + ), + (error) => showToastNotification( + context, + message: '${LocaleKeys.publish_publishFailed.tr()}: ${error.code}', + ), + ); + } else if (state.unpublishResult != null) { + state.unpublishResult!.fold( + (value) => showToastNotification( + context, + message: LocaleKeys.publish_unpublishSuccessfully.tr(), + ), + (error) => showToastNotification( + context, + message: LocaleKeys.publish_unpublishFailed.tr(), + description: error.msg, + ), + ); + } + } +} + +class _PublishedWidget extends StatefulWidget { + const _PublishedWidget({ + required this.url, + required this.onVisitSite, + required this.onUnPublish, + }); + + final String url; + final void Function(String url) onVisitSite; + final VoidCallback onUnPublish; + + @override + State<_PublishedWidget> createState() => _PublishedWidgetState(); +} + +class _PublishedWidgetState extends State<_PublishedWidget> { + final controller = TextEditingController(); + + @override + void initState() { + super.initState(); + controller.text = widget.url; + } + + @override + void dispose() { + controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const VSpace(16), + const _PublishTabHeader(), + const VSpace(16), + _PublishUrl( + controller: controller, + onCopy: (url) { + getIt().setData( + ClipboardServiceData(plainText: url), + ); + + showToastNotification( + context, + message: LocaleKeys.grid_url_copy.tr(), + ); + }, + onSubmitted: (url) {}, + ), + const VSpace(16), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + _buildUnpublishButton(), + const Spacer(), + _buildVisitSiteButton(), + ], + ), + ], + ); + } + + Widget _buildUnpublishButton() { + return SizedBox( + width: 184, + height: 36, + child: FlowyButton( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10), + border: Border.all(color: ShareMenuColors.borderColor(context)), + ), + radius: BorderRadius.circular(10), + text: FlowyText.regular( + LocaleKeys.shareAction_unPublish.tr(), + textAlign: TextAlign.center, + ), + onTap: widget.onUnPublish, + ), + ); + } + + Widget _buildVisitSiteButton() { + return RoundedTextButton( + width: 184, + height: 36, + onPressed: () => widget.onVisitSite(controller.text), + title: LocaleKeys.shareAction_visitSite.tr(), + borderRadius: const BorderRadius.all(Radius.circular(10)), + fillColor: Theme.of(context).colorScheme.primary, + textColor: Theme.of(context).colorScheme.onPrimary, + ); + } +} + +class _PublishWidget extends StatefulWidget { + const _PublishWidget({ + required this.onPublish, + }); + + final void Function(List selectedViews) onPublish; + + @override + State<_PublishWidget> createState() => _PublishWidgetState(); +} + +class _PublishWidgetState extends State<_PublishWidget> { + List _selectedViews = []; + + @override + Widget build(BuildContext context) { + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const VSpace(16), + const _PublishTabHeader(), + const VSpace(16), + // if current view is a database, show the database selector + if (context.read().view.layout.isDatabaseView) ...[ + _PublishDatabaseSelector( + view: context.read().view, + onSelected: (selectedDatabases) { + _selectedViews = selectedDatabases; + }, + ), + const VSpace(16), + ], + _PublishButton( + onPublish: () { + if (context.read().view.layout.isDatabaseView) { + // check if any database is selected + if (_selectedViews.isEmpty) { + showToastNotification( + context, + message: LocaleKeys.publish_noDatabaseSelected.tr(), + ); + return; + } + } + + widget.onPublish(_selectedViews); + }, + ), + ], + ); + } +} + +class _PublishButton extends StatelessWidget { + const _PublishButton({ + required this.onPublish, + }); + + final VoidCallback onPublish; + + @override + Widget build(BuildContext context) { + return RoundedTextButton( + height: 36, + title: LocaleKeys.shareAction_publish.tr(), + padding: const EdgeInsets.symmetric(vertical: 9.0), + fontSize: 14.0, + textColor: Theme.of(context).colorScheme.onPrimary, + onPressed: onPublish, + ); + } +} + +class _PublishTabHeader extends StatelessWidget { + const _PublishTabHeader(); + + @override + Widget build(BuildContext context) { + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + const FlowySvg(FlowySvgs.share_publish_s), + const HSpace(6), + FlowyText(LocaleKeys.shareAction_publishToTheWeb.tr()), + ], + ), + const VSpace(4), + FlowyText.regular( + LocaleKeys.shareAction_publishToTheWebHint.tr(), + fontSize: 12, + maxLines: 3, + color: Theme.of(context).hintColor, + ), + ], + ); + } +} + +class _PublishUrl extends StatelessWidget { + const _PublishUrl({ + required this.controller, + required this.onCopy, + required this.onSubmitted, + }); + + final TextEditingController controller; + final void Function(String url) onCopy; + final void Function(String url) onSubmitted; + + @override + Widget build(BuildContext context) { + return SizedBox( + height: 36, + child: FlowyTextField( + readOnly: true, + autoFocus: false, + controller: controller, + enableBorderColor: ShareMenuColors.borderColor(context), + suffixIcon: _buildCopyLinkIcon(context), + ), + ); + } + + Widget _buildCopyLinkIcon(BuildContext context) { + return FlowyHover( + child: GestureDetector( + onTap: () => onCopy(controller.text), + child: Container( + width: 36, + height: 36, + alignment: Alignment.center, + padding: const EdgeInsets.all(10), + decoration: const BoxDecoration( + border: Border(left: BorderSide(color: Color(0x141F2329))), + ), + child: const FlowySvg( + FlowySvgs.m_toolbar_link_m, + ), + ), + ), + ); + } +} + +// used to select which database view should be published +class _PublishDatabaseSelector extends StatefulWidget { + const _PublishDatabaseSelector({ + required this.view, + required this.onSelected, + }); + + final ViewPB view; + final void Function(List selectedDatabases) onSelected; + + @override + State<_PublishDatabaseSelector> createState() => + _PublishDatabaseSelectorState(); +} + +class _PublishDatabaseSelectorState extends State<_PublishDatabaseSelector> { + final PropertyValueNotifier> _databaseStatus = + PropertyValueNotifier>([]); + late final _borderColor = Theme.of(context).hintColor.withOpacity(0.3); + + @override + void initState() { + super.initState(); + + _databaseStatus.addListener(() { + final selectedDatabases = + _databaseStatus.value.where((e) => e.$2).map((e) => e.$1).toList(); + widget.onSelected(selectedDatabases); + }); + + _databaseStatus.value = context + .read() + .state + .tabBars + .map((e) => (e.view, true)) + .toList(); + } + + @override + void dispose() { + _databaseStatus.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + return Container( + clipBehavior: Clip.antiAlias, + decoration: ShapeDecoration( + shape: RoundedRectangleBorder( + side: BorderSide(color: _borderColor), + borderRadius: BorderRadius.circular(8), + ), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const VSpace(10), + _buildSelectedDatabaseCount(context), + const VSpace(10), + _buildDivider(context), + const VSpace(10), + ...state.tabBars.map( + (e) => _buildDatabaseSelector(context, e), + ), + ], + ), + ); + }, + ); + } + + Widget _buildDivider(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 12), + child: Divider( + color: _borderColor, + thickness: 1, + height: 1, + ), + ); + } + + Widget _buildSelectedDatabaseCount(BuildContext context) { + return ValueListenableBuilder( + valueListenable: _databaseStatus, + builder: (context, selectedDatabases, child) { + final count = selectedDatabases.where((e) => e.$2).length; + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 12), + child: FlowyText( + LocaleKeys.publish_database.plural(count).tr(), + color: Theme.of(context).hintColor, + fontSize: 13, + ), + ); + }, + ); + } + + Widget _buildDatabaseSelector(BuildContext context, DatabaseTabBar tabBar) { + final isPrimaryDatabase = tabBar.view.id == widget.view.id; + return ValueListenableBuilder( + valueListenable: _databaseStatus, + builder: (context, selectedDatabases, child) { + final isSelected = selectedDatabases.any( + (e) => e.$1.id == tabBar.view.id && e.$2, + ); + return _DatabaseSelectorItem( + tabBar: tabBar, + isSelected: isSelected, + isPrimaryDatabase: isPrimaryDatabase, + onTap: () { + // unable to deselect the primary database + if (isPrimaryDatabase) { + showToastNotification( + context, + message: + LocaleKeys.publish_unableToDeselectPrimaryDatabase.tr(), + ); + return; + } + + // toggle the selection status + _databaseStatus.value = _databaseStatus.value + .map( + (e) => + e.$1.id == tabBar.view.id ? (e.$1, !e.$2) : (e.$1, e.$2), + ) + .toList(); + }, + ); + }, + ); + } +} + +class _DatabaseSelectorItem extends StatelessWidget { + const _DatabaseSelectorItem({ + required this.tabBar, + required this.isSelected, + required this.onTap, + required this.isPrimaryDatabase, + }); + + final DatabaseTabBar tabBar; + final bool isSelected; + final VoidCallback onTap; + final bool isPrimaryDatabase; + + @override + Widget build(BuildContext context) { + Widget child = _buildItem(context); + + if (!isPrimaryDatabase) { + child = FlowyHover( + child: GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: onTap, + child: child, + ), + ); + } else { + child = FlowyTooltip( + message: LocaleKeys.publish_mustSelectPrimaryDatabase.tr(), + child: MouseRegion( + cursor: SystemMouseCursors.forbidden, + child: child, + ), + ); + } + + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 2), + child: child, + ); + } + + Widget _buildItem(BuildContext context) { + final svg = isPrimaryDatabase + ? FlowySvgs.unable_select_s + : isSelected + ? FlowySvgs.check_filled_s + : FlowySvgs.uncheck_s; + final blendMode = isPrimaryDatabase ? BlendMode.srcIn : null; + return Container( + height: 30, + padding: const EdgeInsets.symmetric(horizontal: 8), + child: Row( + children: [ + FlowySvg( + svg, + blendMode: blendMode, + size: const Size.square(18), + ), + const HSpace(9.0), + FlowySvg( + tabBar.view.layout.icon, + size: const Size.square(16), + ), + const HSpace(6.0), + FlowyText.regular( + tabBar.view.name, + fontSize: 14, + overflow: TextOverflow.ellipsis, + ), + ], + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/document/application/document_share_bloc.dart b/frontend/appflowy_flutter/lib/plugins/shared/share/share_bloc.dart similarity index 68% rename from frontend/appflowy_flutter/lib/plugins/document/application/document_share_bloc.dart rename to frontend/appflowy_flutter/lib/plugins/shared/share/share_bloc.dart index fbf0a88a0cd03..16d50d22127be 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/application/document_share_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/shared/share/share_bloc.dart @@ -2,10 +2,10 @@ import 'dart:io'; import 'package:appflowy/user/application/user_service.dart'; import 'package:appflowy/workspace/application/export/document_exporter.dart'; +import 'package:appflowy/workspace/application/settings/share/export_service.dart'; import 'package:appflowy/workspace/application/view/view_listener.dart'; import 'package:appflowy/workspace/application/view/view_service.dart'; import 'package:appflowy_backend/log.dart'; -import 'package:appflowy_backend/protobuf/flowy-document/entities.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; @@ -13,31 +13,31 @@ import 'package:appflowy_result/appflowy_result.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; -part 'document_share_bloc.freezed.dart'; +part 'share_bloc.freezed.dart'; const _url = 'https://appflowy.com'; -class DocumentShareBloc extends Bloc { - DocumentShareBloc({ +class ShareBloc extends Bloc { + ShareBloc({ required this.view, - }) : super(DocumentShareState.initial()) { - on((event, emit) async { + }) : super(ShareState.initial()) { + on((event, emit) async { await event.when( initial: () async { viewListener = ViewListener(viewId: view.id) ..start( onViewUpdated: (value) { - add(DocumentShareEvent.updateViewName(value.name)); + add(ShareEvent.updateViewName(value.name)); }, onViewMoveToTrash: (p0) { - add(const DocumentShareEvent.setPublishStatus(false)); + add(const ShareEvent.setPublishStatus(false)); }, ); - add(const DocumentShareEvent.updatePublishStatus()); + add(const ShareEvent.updatePublishStatus()); }, share: (type, path) async { - if (DocumentShareType.unimplemented.contains(type)) { + if (ShareType.unimplemented.contains(type)) { Log.error('DocumentShareType $type is not implemented'); return; } @@ -53,7 +53,7 @@ class DocumentShareBloc extends Bloc { ), ); }, - publish: (nameSpace, publishName) async { + publish: (nameSpace, publishName, selectedViewIds) async { // set space name try { final result = @@ -62,6 +62,7 @@ class DocumentShareBloc extends Bloc { await ViewBackendService.publish( view, name: publishName, + selectedViewIds: selectedViewIds, ).getOrThrow(); emit( @@ -72,6 +73,8 @@ class DocumentShareBloc extends Bloc { url: '$_url/${result.namespace}/$publishName', ), ); + + Log.info('publish success: ${result.namespace}/$publishName'); } catch (e) { Log.error('publish error: $e'); @@ -155,7 +158,7 @@ class DocumentShareBloc extends Bloc { final ViewPB view; late final ViewListener viewListener; - late final exporter = DocumentExporter(view); + late final documentExporter = DocumentExporter(view); @override Future close() async { @@ -163,19 +166,31 @@ class DocumentShareBloc extends Bloc { return super.close(); } - Future> _export( - DocumentShareType type, + Future> _export( + ShareType type, String? path, ) async { - final result = await exporter.export(type.exportType); + final FlowyResult result; + if (type == ShareType.csv) { + final exportResult = await BackendExportService.exportDatabaseAsCSV( + view.id, + ); + result = exportResult.fold( + (s) => FlowyResult.success(s.data), + (f) => FlowyResult.failure(f), + ); + } else { + result = await documentExporter.export(type.documentExportType); + } return result.fold( (s) { if (path != null) { switch (type) { - case DocumentShareType.markdown: - return FlowySuccess(_saveMarkdownToPath(s, path)); - case DocumentShareType.html: - return FlowySuccess(_saveHTMLToPath(s, path)); + case ShareType.markdown: + case ShareType.html: + case ShareType.csv: + File(path).writeAsStringSync(s); + return FlowyResult.success(type); default: break; } @@ -185,77 +200,69 @@ class DocumentShareBloc extends Bloc { (f) => FlowyResult.failure(f), ); } - - ExportDataPB _saveMarkdownToPath(String markdown, String path) { - File(path).writeAsStringSync(markdown); - return ExportDataPB() - ..data = markdown - ..exportType = ExportType.Markdown; - } - - ExportDataPB _saveHTMLToPath(String html, String path) { - File(path).writeAsStringSync(html); - return ExportDataPB() - ..data = html - ..exportType = ExportType.HTML; - } } -enum DocumentShareType { +enum ShareType { + // available in document markdown, html, text, - link; + link, + + // only available in database + csv; - static List get unimplemented => [text, link]; + static List get unimplemented => [link]; - DocumentExportType get exportType { + DocumentExportType get documentExportType { switch (this) { - case DocumentShareType.markdown: + case ShareType.markdown: return DocumentExportType.markdown; - case DocumentShareType.html: + case ShareType.html: return DocumentExportType.html; - case DocumentShareType.text: + case ShareType.text: return DocumentExportType.text; - case DocumentShareType.link: + case ShareType.csv: + throw UnsupportedError('DocumentShareType.csv is not supported'); + case ShareType.link: throw UnsupportedError('DocumentShareType.link is not supported'); } } } @freezed -class DocumentShareEvent with _$DocumentShareEvent { - const factory DocumentShareEvent.initial() = _Initial; - const factory DocumentShareEvent.share( - DocumentShareType type, +class ShareEvent with _$ShareEvent { + const factory ShareEvent.initial() = _Initial; + const factory ShareEvent.share( + ShareType type, String? path, ) = _Share; - const factory DocumentShareEvent.publish( + const factory ShareEvent.publish( String nameSpace, String pageId, + List selectedViewIds, ) = _Publish; - const factory DocumentShareEvent.unPublish() = _UnPublish; - const factory DocumentShareEvent.updateViewName(String name) = - _UpdateViewName; - const factory DocumentShareEvent.updatePublishStatus() = _UpdatePublishStatus; - const factory DocumentShareEvent.setPublishStatus(bool isPublished) = + const factory ShareEvent.unPublish() = _UnPublish; + const factory ShareEvent.updateViewName(String name) = _UpdateViewName; + const factory ShareEvent.updatePublishStatus() = _UpdatePublishStatus; + const factory ShareEvent.setPublishStatus(bool isPublished) = _SetPublishStatus; } @freezed -class DocumentShareState with _$DocumentShareState { - const factory DocumentShareState({ +class ShareState with _$ShareState { + const factory ShareState({ required bool isPublished, required bool isLoading, required String url, required String viewName, required bool enablePublish, - FlowyResult? exportResult, + FlowyResult? exportResult, FlowyResult? publishResult, FlowyResult? unpublishResult, - }) = _DocumentShareState; + }) = _ShareState; - factory DocumentShareState.initial() => const DocumentShareState( + factory ShareState.initial() => const ShareState( isLoading: false, isPublished: false, enablePublish: true, diff --git a/frontend/appflowy_flutter/lib/plugins/shared/share/share_button.dart b/frontend/appflowy_flutter/lib/plugins/shared/share/share_button.dart new file mode 100644 index 0000000000000..704205f9a021d --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/shared/share/share_button.dart @@ -0,0 +1,82 @@ +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/plugins/database/application/tab_bar_bloc.dart'; +import 'package:appflowy/plugins/shared/share/_shared.dart'; +import 'package:appflowy/plugins/shared/share/share_bloc.dart'; +import 'package:appflowy/plugins/shared/share/share_menu.dart'; +import 'package:appflowy/startup/startup.dart'; +import 'package:appflowy/workspace/application/view/view_ext.dart'; +import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; +import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class ShareButton extends StatelessWidget { + const ShareButton({ + super.key, + required this.view, + }); + + final ViewPB view; + + @override + Widget build(BuildContext context) { + return MultiBlocProvider( + providers: [ + BlocProvider( + create: (context) => + getIt(param1: view)..add(const ShareEvent.initial()), + ), + if (view.layout.isDatabaseView) + BlocProvider( + create: (context) => DatabaseTabBarBloc(view: view) + ..add(const DatabaseTabBarEvent.initial()), + ), + ], + child: BlocListener( + listener: (context, state) { + if (!state.isLoading && state.exportResult != null) { + state.exportResult!.fold( + (data) => _handleExportSuccess(context, data), + (error) => _handleExportError(context, error), + ); + } + }, + child: BlocBuilder( + builder: (context, state) { + final tabs = [ + if (state.enablePublish) ShareMenuTab.publish, + ShareMenuTab.exportAs, + ]; + + return ShareMenuButton(tabs: tabs); + }, + ), + ), + ); + } + + void _handleExportSuccess(BuildContext context, ShareType shareType) { + switch (shareType) { + case ShareType.markdown: + case ShareType.html: + case ShareType.csv: + showToastNotification( + context, + message: LocaleKeys.settings_files_exportFileSuccess.tr(), + ); + break; + default: + break; + } + } + + void _handleExportError(BuildContext context, FlowyError error) { + showToastNotification( + context, + message: + '${LocaleKeys.settings_files_exportFileFail.tr()}: ${error.code}', + ); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/share/share_menu.dart b/frontend/appflowy_flutter/lib/plugins/shared/share/share_menu.dart similarity index 94% rename from frontend/appflowy_flutter/lib/plugins/document/presentation/share/share_menu.dart rename to frontend/appflowy_flutter/lib/plugins/shared/share/share_menu.dart index 2c27f4135d355..b687f51036abe 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/share/share_menu.dart +++ b/frontend/appflowy_flutter/lib/plugins/shared/share/share_menu.dart @@ -1,8 +1,8 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/mobile/presentation/home/tab/_round_underline_tab_indicator.dart'; -import 'package:appflowy/plugins/document/application/document_share_bloc.dart'; -import 'package:appflowy/plugins/document/presentation/share/export_tab.dart'; +import 'package:appflowy/plugins/shared/share/export_tab.dart'; +import 'package:appflowy/plugins/shared/share/share_bloc.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; @@ -165,7 +165,7 @@ class _SegmentState extends State<_Segment> { ); if (widget.tab == ShareMenuTab.publish) { - final isPublished = context.watch().state.isPublished; + final isPublished = context.watch().state.isPublished; // show checkmark icon if published if (isPublished) { child = Row( diff --git a/frontend/appflowy_flutter/lib/plugins/trash/trash.dart b/frontend/appflowy_flutter/lib/plugins/trash/trash.dart index e2f312373e9a4..22bb1b37d7b3f 100644 --- a/frontend/appflowy_flutter/lib/plugins/trash/trash.dart +++ b/frontend/appflowy_flutter/lib/plugins/trash/trash.dart @@ -1,18 +1,18 @@ -export "./src/sizes.dart"; -export "./src/trash_cell.dart"; -export "./src/trash_header.dart"; - import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/startup/plugin/plugin.dart'; import 'package:appflowy/workspace/presentation/home/home_stack.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pbenum.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/style_widget/text.dart'; import 'package:flutter/material.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; import 'trash_page.dart'; +export "./src/sizes.dart"; +export "./src/trash_cell.dart"; +export "./src/trash_header.dart"; + class TrashPluginBuilder extends PluginBuilder { @override Plugin build(dynamic data) { @@ -66,6 +66,7 @@ class TrashPluginDisplay extends PluginWidgetBuilder { Widget buildWidget({ required PluginContext context, required bool shrinkWrap, + Map? data, }) => const TrashPage( key: ValueKey('TrashPage'), diff --git a/frontend/appflowy_flutter/lib/startup/deps_resolver.dart b/frontend/appflowy_flutter/lib/startup/deps_resolver.dart index 89049fb08a3b9..59fa0b1025b20 100644 --- a/frontend/appflowy_flutter/lib/startup/deps_resolver.dart +++ b/frontend/appflowy_flutter/lib/startup/deps_resolver.dart @@ -182,8 +182,8 @@ void _resolveHomeDeps(GetIt getIt) { ); // share - getIt.registerFactoryParam( - (view, _) => DocumentShareBloc(view: view), + getIt.registerFactoryParam( + (view, _) => ShareBloc(view: view), ); getIt.registerSingleton(ActionNavigationBloc()); diff --git a/frontend/appflowy_flutter/lib/startup/plugin/plugin.dart b/frontend/appflowy_flutter/lib/startup/plugin/plugin.dart index b81860cd99c30..920a994927baf 100644 --- a/frontend/appflowy_flutter/lib/startup/plugin/plugin.dart +++ b/frontend/appflowy_flutter/lib/startup/plugin/plugin.dart @@ -1,13 +1,12 @@ library flowy_plugin; -import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart'; -import 'package:flutter/widgets.dart'; - import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/startup/plugin/plugin.dart'; import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/workspace/presentation/home/home_stack.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart'; +import 'package:flutter/widgets.dart'; export "./src/sandbox.dart"; @@ -76,6 +75,7 @@ abstract class PluginWidgetBuilder with NavigationItem { Widget buildWidget({ required PluginContext context, required bool shrinkWrap, + Map? data, }); } diff --git a/frontend/appflowy_flutter/lib/workspace/application/menu/sidebar_sections_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/menu/sidebar_sections_bloc.dart index 0da3d973dbed5..48422720bac7c 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/menu/sidebar_sections_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/menu/sidebar_sections_bloc.dart @@ -106,6 +106,8 @@ class SidebarSectionsBloc case ViewSectionPB.Public: emit( state.copyWith( + containsSpace: state.containsSpace || + sectionViews.views.any((view) => view.isSpace), section: state.section.copyWith( publicViews: sectionViews.views, ), @@ -114,6 +116,8 @@ class SidebarSectionsBloc case ViewSectionPB.Private: emit( state.copyWith( + containsSpace: state.containsSpace || + sectionViews.views.any((view) => view.isSpace), section: state.section.copyWith( privateViews: sectionViews.views, ), diff --git a/frontend/appflowy_flutter/lib/workspace/application/view/view_ext.dart b/frontend/appflowy_flutter/lib/workspace/application/view/view_ext.dart index 888f91897a868..3dc87b0d78ce3 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/view/view_ext.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/view/view_ext.dart @@ -231,7 +231,7 @@ extension ViewLayoutExtension on ViewLayoutPB { FlowySvgData get icon => switch (this) { ViewLayoutPB.Grid => FlowySvgs.grid_s, ViewLayoutPB.Board => FlowySvgs.board_s, - ViewLayoutPB.Calendar => FlowySvgs.date_s, + ViewLayoutPB.Calendar => FlowySvgs.calendar_s, ViewLayoutPB.Document => FlowySvgs.document_s, ViewLayoutPB.Chat => FlowySvgs.chat_ai_page_s, _ => throw Exception('Unknown layout type'), diff --git a/frontend/appflowy_flutter/lib/workspace/application/view/view_service.dart b/frontend/appflowy_flutter/lib/workspace/application/view/view_service.dart index d1c865937074a..ccdcb4697ccd0 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/view/view_service.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/view/view_service.dart @@ -302,12 +302,18 @@ class ViewBackendService { static Future> publish( ViewPB view, { String? name, + List? selectedViewIds, }) async { final payload = PublishViewParamsPB()..viewId = view.id; if (name != null) { payload.publishName = name; } + + if (selectedViewIds != null && selectedViewIds.isNotEmpty) { + payload.selectedViewIds = RepeatedViewIdPB(items: selectedViewIds); + } + return FolderEventPublishView(payload).send(); } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar.dart index 47bfc237d84b3..7c6761d8007f2 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar.dart @@ -342,7 +342,14 @@ class _SidebarState extends State<_Sidebar> { child: const Divider(height: 0.5, color: Color(0x141F2329)), ), const VSpace(8), - _renderUpgradeSpaceButton(menuHorizontalInset), + + Column( + children: [ + // ai pay button + _renderUpgradeSpaceButton(menuHorizontalInset), + ], + ), + const VSpace(8), Padding( padding: menuHorizontalInset + diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/notifications/widgets/notification_item.dart b/frontend/appflowy_flutter/lib/workspace/presentation/notifications/widgets/notification_item.dart index 5abc214583889..7076e6037ed2c 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/notifications/widgets/notification_item.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/notifications/widgets/notification_item.dart @@ -1,5 +1,3 @@ -import 'package:flutter/material.dart'; - import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/document/presentation/editor_configuration.dart'; @@ -14,6 +12,7 @@ import 'package:fixnum/fixnum.dart'; import 'package:flowy_infra/size.dart'; import 'package:flowy_infra/theme_extension.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; class NotificationItem extends StatefulWidget { @@ -239,19 +238,22 @@ class _NotificationContent extends StatelessWidget { padding: EdgeInsets.zero, ); - return Transform.scale( - scale: .9, - alignment: Alignment.centerLeft, - child: AppFlowyEditor( - editorState: editorState, - editorStyle: styleCustomizer.style(), - editable: false, - shrinkWrap: true, - blockComponentBuilders: getEditorBuilderMap( - context: context, + return ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 200), + child: Transform.scale( + scale: .9, + alignment: Alignment.centerLeft, + child: AppFlowyEditor( editorState: editorState, - styleCustomizer: styleCustomizer, + editorStyle: styleCustomizer.style(), editable: false, + shrinkWrap: true, + blockComponentBuilders: getEditorBuilderMap( + context: context, + editorState: editorState, + styleCustomizer: styleCustomizer, + editable: false, + ), ), ), ); diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/dialogs.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/dialogs.dart index 67062450cf14a..2533b61a0e4b9 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/dialogs.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/dialogs.dart @@ -298,7 +298,10 @@ void showToastNotification( context: context, type: type, style: ToastificationStyle.flat, - title: FlowyText(message), + title: FlowyText( + message, + maxLines: 3, + ), description: description != null ? FlowyText.regular( description, diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/float_bubble/question_bubble.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/float_bubble/question_bubble.dart index 6ff84a9adca1a..0f7a5d06230bc 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/float_bubble/question_bubble.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/float_bubble/question_bubble.dart @@ -14,6 +14,7 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/style_widget/text.dart'; import 'package:flowy_infra_ui/widget/flowy_tooltip.dart'; import 'package:flowy_infra_ui/widget/spacing.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:package_info_plus/package_info_plus.dart'; @@ -209,7 +210,7 @@ class FlowyVersionDescription extends CustomActionCell { GestureDetector( behavior: HitTestBehavior.opaque, onDoubleTap: () { - if (Env.internalBuild != '1') { + if (Env.internalBuild != '1' && !kDebugMode) { return; } enableDocumentInternalLog = !enableDocumentInternalLog; diff --git a/frontend/appflowy_flutter/packages/appflowy_result/lib/src/async_result.dart b/frontend/appflowy_flutter/packages/appflowy_result/lib/src/async_result.dart index 94cd9a68a61b0..e8396d8019ba8 100644 --- a/frontend/appflowy_flutter/packages/appflowy_result/lib/src/async_result.dart +++ b/frontend/appflowy_flutter/packages/appflowy_result/lib/src/async_result.dart @@ -34,4 +34,8 @@ extension FlowyAsyncResultExtension FlowyAsyncResult onFailure(void Function(F failure) onFailure) { return then((result) => result..onFailure(onFailure)); } + + FlowyAsyncResult onSuccess(void Function(S success) onSuccess) { + return then((result) => result..onSuccess(onSuccess)); + } } diff --git a/frontend/resources/flowy_icons/16x/unable_select.svg b/frontend/resources/flowy_icons/16x/unable_select.svg new file mode 100644 index 0000000000000..0c661c79a4c5e --- /dev/null +++ b/frontend/resources/flowy_icons/16x/unable_select.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index 854f04689c014..9d58581071b14 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -2087,7 +2087,17 @@ "noAccessToVisit": "No access to this page...", "createWithAppFlowy": "Create a website with AppFlowy", "fastWithAI": "Fast and easy with AI.", - "tryItNow": "Try it now" + "tryItNow": "Try it now", + "onlyGridViewCanBePublished": "Only Grid view can be published", + "database": { + "zero": "Publish {} selected view", + "one": "Publish {} selected views", + "many": "Publish {} selected views", + "other":"Publish {} selected views" + }, + "mustSelectPrimaryDatabase": "The primary database must be selected", + "noDatabaseSelected": "No database selected, please select at least one database.", + "unableToDeselectPrimaryDatabase": "Unable to deselect primary database" }, "web": { "continue": "Continue", diff --git a/frontend/rust-lib/event-integration-test/src/folder_event.rs b/frontend/rust-lib/event-integration-test/src/folder_event.rs index 690cb81534868..0c554df4b297e 100644 --- a/frontend/rust-lib/event-integration-test/src/folder_event.rs +++ b/frontend/rust-lib/event-integration-test/src/folder_event.rs @@ -1,4 +1,4 @@ -use collab::entity::EncodedCollab; +use flowy_folder::view_operation::EncodedCollabWrapper; use std::sync::Arc; use collab_folder::{FolderData, View}; @@ -6,7 +6,7 @@ use flowy_folder::entities::icon::UpdateViewIconPayloadPB; use flowy_folder::event_map::FolderEvent; use flowy_folder::event_map::FolderEvent::*; use flowy_folder::{entities::*, ViewLayout}; -use flowy_folder_pub::entities::PublishViewPayload; +use flowy_folder_pub::entities::PublishPayload; use flowy_search::services::manager::{SearchHandler, SearchType}; use flowy_user::entities::{ AcceptWorkspaceInvitationPB, QueryWorkspacePB, RemoveWorkspaceMemberPB, @@ -177,8 +177,8 @@ impl EventIntegrationTest { pub async fn get_publish_payload( &self, view_id: &str, - include_children: Option, - ) -> Vec { + include_children: bool, + ) -> Vec { let manager = self.folder_manager.clone(); let payload = manager .get_batch_publish_payload(view_id, None, include_children) @@ -191,11 +191,19 @@ impl EventIntegrationTest { payload.unwrap() } - pub async fn encoded_collab_v1(&self, view_id: &str, layout: ViewLayout) -> EncodedCollab { + pub async fn get_encoded_collab_v1_from_disk( + &self, + view_id: &str, + layout: ViewLayout, + ) -> EncodedCollabWrapper { let manager = self.folder_manager.clone(); + let user = manager.get_user().clone(); let handlers = manager.get_operation_handlers(); let handler = handlers.get(&layout).unwrap(); - handler.encoded_collab_v1(view_id, layout).await.unwrap() + handler + .get_encoded_collab_v1_from_disk(user, view_id) + .await + .unwrap() } pub async fn get_all_workspace_views(&self) -> Vec { @@ -207,6 +215,16 @@ impl EventIntegrationTest { .items } + // get all the views in the current workspace, including the views in the trash and the orphan views + pub async fn get_all_views(&self) -> Vec { + EventBuilder::new(self.clone()) + .event(FolderEvent::GetAllViews) + .async_send() + .await + .parse::() + .items + } + pub async fn get_trash(&self) -> RepeatedTrashPB { EventBuilder::new(self.clone()) .event(FolderEvent::ListTrashItems) @@ -252,12 +270,23 @@ impl EventIntegrationTest { } pub async fn create_view(&self, parent_id: &str, name: String) -> ViewPB { + self + .create_view_with_layout(parent_id, name, Default::default()) + .await + } + + pub async fn create_view_with_layout( + &self, + parent_id: &str, + name: String, + layout: ViewLayoutPB, + ) -> ViewPB { let payload = CreateViewPayloadPB { parent_view_id: parent_id.to_string(), name, desc: "".to_string(), thumbnail: None, - layout: Default::default(), + layout, initial_data: vec![], meta: Default::default(), set_as_current: false, diff --git a/frontend/rust-lib/event-integration-test/tests/asset/064_database_publish.zip b/frontend/rust-lib/event-integration-test/tests/asset/064_database_publish.zip new file mode 100644 index 0000000000000..835bfdf52789e Binary files /dev/null and b/frontend/rust-lib/event-integration-test/tests/asset/064_database_publish.zip differ diff --git a/frontend/rust-lib/event-integration-test/tests/asset/publish_grid_primary.csv.zip b/frontend/rust-lib/event-integration-test/tests/asset/publish_grid_primary.csv.zip new file mode 100644 index 0000000000000..785449c8bedc8 Binary files /dev/null and b/frontend/rust-lib/event-integration-test/tests/asset/publish_grid_primary.csv.zip differ diff --git a/frontend/rust-lib/event-integration-test/tests/folder/local_test/mod.rs b/frontend/rust-lib/event-integration-test/tests/folder/local_test/mod.rs index 2c48d266f7fb9..66a0a62937965 100644 --- a/frontend/rust-lib/event-integration-test/tests/folder/local_test/mod.rs +++ b/frontend/rust-lib/event-integration-test/tests/folder/local_test/mod.rs @@ -3,4 +3,6 @@ mod import_test; mod script; mod subscription_test; mod test; -mod view_publish_test; + +mod publish_database_test; +mod publish_document_test; diff --git a/frontend/rust-lib/event-integration-test/tests/folder/local_test/publish_database_test.rs b/frontend/rust-lib/event-integration-test/tests/folder/local_test/publish_database_test.rs new file mode 100644 index 0000000000000..cbf814d6f2b4f --- /dev/null +++ b/frontend/rust-lib/event-integration-test/tests/folder/local_test/publish_database_test.rs @@ -0,0 +1,156 @@ +use std::collections::HashMap; + +use collab_folder::ViewLayout; +use event_integration_test::EventIntegrationTest; +use flowy_folder::entities::{ + ImportPayloadPB, ImportTypePB, ImportValuePayloadPB, ViewLayoutPB, ViewPB, +}; +use flowy_folder::view_operation::EncodedCollabWrapper; + +use crate::util::unzip; + +#[tokio::test] +async fn publish_single_database_test() { + let test = EventIntegrationTest::new_anon().await; + test.sign_up_as_anon().await; + + // import a csv file and try to get its publish collab + let grid = import_csv("publish_grid_primary.csv", &test).await; + + let grid_encoded_collab = test + .get_encoded_collab_v1_from_disk(&grid.id, ViewLayout::Grid) + .await; + + match grid_encoded_collab { + EncodedCollabWrapper::Database(encoded_collab) => { + // the len of row collabs should be the same as the number of rows in the csv file + let rows_len = encoded_collab.database_row_encoded_collabs.len(); + assert_eq!(rows_len, 18); + }, + _ => panic!("Expected database collab"), + } +} + +#[tokio::test] +async fn publish_databases_from_existing_workspace() { + let test = EventIntegrationTest::new_anon().await; + test.sign_up_as_anon().await; + + // import a workspace + // there's a sample screenshot of the workspace in the asset folder, + // unzip it and check the views if needed + let _ = import_workspace("064_database_publish", &test).await; + + let publish_database_set = test.get_all_views().await; + + let publish_grid_set = publish_database_set + .iter() + // there're 8 built-in grids in the workspace with the name starting with "publish grid" + .filter(|view| view.layout == ViewLayoutPB::Grid && view.name.starts_with("publish grid")) + .collect::>(); + + let publish_calendar_set = publish_database_set + .iter() + // there's 1 built-in calender in the workspace with the name starting with "publish calendar" + .filter(|view| view.layout == ViewLayoutPB::Calendar && view.name.starts_with("publish calendar")) + .collect::>(); + + let publish_board_set = publish_database_set + .iter() + // there's 1 built-in board in the workspace with the name starting with "publish board" + .filter(|view| view.layout == ViewLayoutPB::Board && view.name.starts_with("publish board")) + .collect::>(); + + let mut expectations: HashMap<&str, usize> = HashMap::new(); + // grid + // 5 rows + expectations.insert("publish grid (deprecated)", 5); + + // the following 7 grids are the same, just with different filters or sorting or layout + // to check if the collab is correctly generated + // 18 rows + expectations.insert("publish grid", 18); + // 18 rows + expectations.insert("publish grid (with board)", 18); + // 18 rows + expectations.insert("publish grid (with calendar)", 18); + // 18 rows + expectations.insert("publish grid (with grid)", 18); + // 18 rows + expectations.insert("publish grid (filtered)", 18); + // 18 rows + expectations.insert("publish grid (sorted)", 18); + + // calendar + expectations.insert("publish calendar", 2); + + // board + expectations.insert("publish board", 15); + + test_publish_encode_collab_result(&test, publish_grid_set, expectations.clone()).await; + + test_publish_encode_collab_result(&test, publish_calendar_set, expectations.clone()).await; + + test_publish_encode_collab_result(&test, publish_board_set, expectations.clone()).await; +} + +async fn test_publish_encode_collab_result( + test: &EventIntegrationTest, + views: Vec<&ViewPB>, + expectations: HashMap<&str, usize>, +) { + for view in views { + let id = view.id.clone(); + let layout = view.layout.clone(); + + test.open_database(&id).await; + + let encoded_collab = test + .get_encoded_collab_v1_from_disk(&id, layout.into()) + .await; + + match encoded_collab { + EncodedCollabWrapper::Database(encoded_collab) => { + if let Some(rows_len) = expectations.get(&view.name.as_str()) { + assert_eq!(encoded_collab.database_row_encoded_collabs.len(), *rows_len); + } + }, + _ => panic!("Expected database collab"), + } + } +} + +async fn import_workspace(file_name: &str, test: &EventIntegrationTest) -> Vec { + let (cleaner, file_path) = unzip("./tests/asset", file_name).unwrap(); + test + .import_appflowy_data(file_path.to_str().unwrap().to_string(), None) + .await + .unwrap(); + let views = test.get_all_workspace_views().await; + drop(cleaner); + views +} + +async fn import_csv(file_name: &str, test: &EventIntegrationTest) -> ViewPB { + let (cleaner, file_path) = unzip("./tests/asset", file_name).unwrap(); + let csv_string = std::fs::read_to_string(file_path).unwrap(); + let workspace_id = test.get_current_workspace().await.id; + let import_data = gen_import_data(file_name.to_string(), csv_string, workspace_id); + let views = test.import_data(import_data).await; + drop(cleaner); + views[0].clone() +} + +fn gen_import_data(file_name: String, csv_string: String, workspace_id: String) -> ImportPayloadPB { + ImportPayloadPB { + parent_view_id: workspace_id.clone(), + sync_after_create: false, + values: vec![ImportValuePayloadPB { + name: file_name, + data: Some(csv_string.as_bytes().to_vec()), + file_path: None, + view_layout: ViewLayoutPB::Grid, + import_type: ImportTypePB::CSV, + }], + } +} diff --git a/frontend/rust-lib/event-integration-test/tests/folder/local_test/view_publish_test.rs b/frontend/rust-lib/event-integration-test/tests/folder/local_test/publish_document_test.rs similarity index 70% rename from frontend/rust-lib/event-integration-test/tests/folder/local_test/view_publish_test.rs rename to frontend/rust-lib/event-integration-test/tests/folder/local_test/publish_document_test.rs index 6e6e6fa9cb486..268e5635fd8f7 100644 --- a/frontend/rust-lib/event-integration-test/tests/folder/local_test/view_publish_test.rs +++ b/frontend/rust-lib/event-integration-test/tests/folder/local_test/publish_document_test.rs @@ -2,18 +2,19 @@ use collab_folder::ViewLayout; use event_integration_test::EventIntegrationTest; use flowy_folder::entities::{ViewLayoutPB, ViewPB}; use flowy_folder::publish_util::generate_publish_name; +use flowy_folder::view_operation::EncodedCollabWrapper; use flowy_folder_pub::entities::{ - PublishViewInfo, PublishViewMeta, PublishViewMetaData, PublishViewPayload, + PublishDocumentPayload, PublishPayload, PublishViewInfo, PublishViewMeta, PublishViewMetaData, }; async fn mock_single_document_view_publish_payload( test: &EventIntegrationTest, view: &ViewPB, publish_name: String, -) -> Vec { +) -> Vec { let view_id = &view.id; let layout: ViewLayout = view.layout.clone().into(); - let view_encoded_collab = test.encoded_collab_v1(view_id, layout).await; + let view_encoded_collab = test.get_encoded_collab_v1_from_disk(view_id, layout).await; let publish_view_info = PublishViewInfo { view_id: view_id.to_string(), name: view.name.to_string(), @@ -27,7 +28,12 @@ async fn mock_single_document_view_publish_payload( child_views: None, }; - vec![PublishViewPayload { + let data = match view_encoded_collab { + EncodedCollabWrapper::Document(doc) => doc.document_encoded_collab.doc_state.to_vec(), + _ => panic!("Expected document collab"), + }; + + vec![PublishPayload::Document(PublishDocumentPayload { meta: PublishViewMeta { metadata: PublishViewMetaData { view: publish_view_info.clone(), @@ -37,18 +43,18 @@ async fn mock_single_document_view_publish_payload( view_id: view_id.to_string(), publish_name, }, - data: Vec::from(view_encoded_collab.doc_state), - }] + data, + })] } async fn mock_nested_document_view_publish_payload( test: &EventIntegrationTest, view: &ViewPB, publish_name: String, -) -> Vec { +) -> Vec { let view_id = &view.id; let layout: ViewLayout = view.layout.clone().into(); - let view_encoded_collab = test.encoded_collab_v1(view_id, layout).await; + let view_encoded_collab = test.get_encoded_collab_v1_from_disk(view_id, layout).await; let publish_view_info = PublishViewInfo { view_id: view_id.to_string(), name: view.name.to_string(), @@ -65,7 +71,9 @@ async fn mock_nested_document_view_publish_payload( let child_view_id = &view.child_views[0].id; let child_view = test.get_view(child_view_id).await; let child_layout: ViewLayout = child_view.layout.clone().into(); - let child_view_encoded_collab = test.encoded_collab_v1(child_view_id, child_layout).await; + let child_view_encoded_collab = test + .get_encoded_collab_v1_from_disk(child_view_id, child_layout) + .await; let child_publish_view_info = PublishViewInfo { view_id: child_view_id.to_string(), name: child_view.name.to_string(), @@ -80,8 +88,18 @@ async fn mock_nested_document_view_publish_payload( }; let child_publish_name = generate_publish_name(&child_view.id, &child_view.name); + let data = match view_encoded_collab { + EncodedCollabWrapper::Document(doc) => doc.document_encoded_collab.doc_state.to_vec(), + _ => panic!("Expected document collab"), + }; + + let child_data = match child_view_encoded_collab { + EncodedCollabWrapper::Document(doc) => doc.document_encoded_collab.doc_state.to_vec(), + _ => panic!("Expected document collab"), + }; + vec![ - PublishViewPayload { + PublishPayload::Document(PublishDocumentPayload { meta: PublishViewMeta { metadata: PublishViewMetaData { view: publish_view_info.clone(), @@ -91,9 +109,9 @@ async fn mock_nested_document_view_publish_payload( view_id: view_id.to_string(), publish_name, }, - data: Vec::from(view_encoded_collab.doc_state), - }, - PublishViewPayload { + data, + }), + PublishPayload::Document(PublishDocumentPayload { meta: PublishViewMeta { metadata: PublishViewMetaData { view: child_publish_view_info.clone(), @@ -103,8 +121,8 @@ async fn mock_nested_document_view_publish_payload( view_id: child_view_id.to_string(), publish_name: child_publish_name, }, - data: Vec::from(child_view_encoded_collab.doc_state), - }, + data: child_data, + }), ] } @@ -126,7 +144,7 @@ async fn single_document_get_publish_view_payload_test() { let name = "Orphan View"; create_single_document(&test, view_id, name).await; let view = test.get_view(view_id).await; - let payload = test.get_publish_payload(view_id, Some(true)).await; + let payload = test.get_publish_payload(view_id, true).await; let expect_payload = mock_single_document_view_publish_payload( &test, @@ -145,7 +163,7 @@ async fn nested_document_get_publish_view_payload_test() { let view_id = "20240521"; create_nested_document(&test, view_id, name).await; let view = test.get_view(view_id).await; - let payload = test.get_publish_payload(view_id, Some(true)).await; + let payload = test.get_publish_payload(view_id, true).await; let expect_payload = mock_nested_document_view_publish_payload( &test, @@ -165,7 +183,7 @@ async fn no_children_publish_view_payload_test() { let view_id = "20240521"; create_nested_document(&test, view_id, name).await; let view = test.get_view(view_id).await; - let payload = test.get_publish_payload(view_id, Some(false)).await; + let payload = test.get_publish_payload(view_id, false).await; let data = mock_single_document_view_publish_payload( &test, @@ -174,7 +192,10 @@ async fn no_children_publish_view_payload_test() { ) .await .iter() - .map(|p| p.data.clone()) + .filter_map(|p| match p { + PublishPayload::Document(payload) => Some(payload.data.clone()), + _ => None, + }) .collect::>(); let meta = mock_nested_document_view_publish_payload( &test, @@ -183,10 +204,24 @@ async fn no_children_publish_view_payload_test() { ) .await .iter() - .map(|p| p.meta.clone()) + .filter_map(|p| match p { + PublishPayload::Document(payload) => Some(payload.meta.clone()), + _ => None, + }) .collect::>(); assert_eq!(payload.len(), 1); - assert_eq!(&payload[0].data, &data[0]); - assert_eq!(&payload[0].meta, &meta[0]); + + let payload_data = match &payload[0] { + PublishPayload::Document(payload) => payload.data.clone(), + _ => panic!("Expected document payload"), + }; + + let payload_meta = match &payload[0] { + PublishPayload::Document(payload) => payload.meta.clone(), + _ => panic!("Expected document payload"), + }; + + assert_eq!(&payload_data, &data[0]); + assert_eq!(&payload_meta, &meta[0]); } diff --git a/frontend/rust-lib/flowy-core/src/deps_resolve/folder_deps.rs b/frontend/rust-lib/flowy-core/src/deps_resolve/folder_deps.rs index cba9ea7d458b2..edc3a7ff8549d 100644 --- a/frontend/rust-lib/flowy-core/src/deps_resolve/folder_deps.rs +++ b/frontend/rust-lib/flowy-core/src/deps_resolve/folder_deps.rs @@ -1,5 +1,6 @@ use bytes::Bytes; -use collab_entity::EncodedCollab; + +use collab_entity::{CollabType, EncodedCollab}; use collab_integrate::collab_builder::AppFlowyCollabBuilder; use collab_integrate::CollabKVDB; use flowy_chat::chat_manager::ChatManager; @@ -15,13 +16,15 @@ use flowy_folder::entities::{CreateViewParams, ViewLayoutPB}; use flowy_folder::manager::{FolderManager, FolderUser}; use flowy_folder::share::ImportType; use flowy_folder::view_operation::{ - FolderOperationHandler, FolderOperationHandlers, View, ViewData, + DatabaseEncodedCollab, DocumentEncodedCollab, EncodedCollabWrapper, FolderOperationHandler, + FolderOperationHandlers, View, ViewData, }; use flowy_folder::ViewLayout; use flowy_folder_pub::folder_builder::NestedViewBuilder; use flowy_search::folder::indexer::FolderIndexManagerImpl; use flowy_sqlite::kv::KVStorePreferences; use flowy_user::services::authenticate_user::AuthenticateUser; +use flowy_user::services::data_import::{load_collab_by_object_id, load_collab_by_object_ids}; use lib_dispatch::prelude::ToBytes; use lib_infra::future::FutureResult; use std::collections::HashMap; @@ -31,6 +34,8 @@ use tokio::sync::RwLock; use crate::integrate::server::ServerProvider; +use collab_plugins::local_storage::kv::KVTransactionDB; + pub struct FolderDepsResolver(); #[allow(clippy::too_many_arguments)] impl FolderDepsResolver { @@ -198,18 +203,46 @@ impl FolderOperationHandler for DocumentFolderOperation { }) } - fn encoded_collab_v1( + fn get_encoded_collab_v1_from_disk( &self, + user: Arc, view_id: &str, - layout: ViewLayout, - ) -> FutureResult { - debug_assert_eq!(layout, ViewLayout::Document); - let view_id = view_id.to_string(); - let manager = self.0.clone(); - FutureResult::new(async move { - let encoded_collab = manager.encode_collab(&view_id).await?; + ) -> FutureResult { + // get the collab_object_id for the document. + // the collab_object_id for the document is the view_id. + let oid = view_id.to_string(); - Ok(encoded_collab) + FutureResult::new(async move { + let uid = user + .user_id() + .map_err(|e| e.with_context("unable to get the uid: {}"))?; + + // get the collab db + let collab_db = user + .collab_db(uid) + .map_err(|e| e.with_context("unable to get the collab"))?; + let collab_db = collab_db.upgrade().ok_or_else(|| { + FlowyError::internal().with_context( + "The collab db has been dropped, indicating that the user has switched to a new account", + ) + })?; + let collab_read_txn = collab_db.read_txn(); + + // read the collab from the db + let collab = load_collab_by_object_id(uid, &collab_read_txn, &oid).map_err(|e| { + FlowyError::internal().with_context(format!("load document collab failed: {}", e)) + })?; + + let encoded_collab = collab + // encode the collab and check the integrity of the collab + .encode_collab_v1(|collab| CollabType::Document.validate_require_data(collab)) + .map_err(|e| { + FlowyError::internal().with_context(format!("encode document collab failed: {}", e)) + })?; + + Ok(EncodedCollabWrapper::Document(DocumentEncodedCollab { + document_encoded_collab: encoded_collab, + })) }) } @@ -300,13 +333,86 @@ impl FolderOperationHandler for DatabaseFolderOperation { }) } - fn encoded_collab_v1( + fn get_encoded_collab_v1_from_disk( &self, - _view_id: &str, - _layout: ViewLayout, - ) -> FutureResult { - // Database view doesn't support collab - FutureResult::new(async move { Err(FlowyError::not_support()) }) + user: Arc, + view_id: &str, + ) -> FutureResult { + let manager = self.0.clone(); + let view_id = view_id.to_string(); + + FutureResult::new(async move { + // get the collab_object_id for the database. + // + // the collab object_id for the database is not the view_id, + // we should use the view_id to get the database_id + let oid = manager.get_database_id_with_view_id(&view_id).await?; + let row_oids = manager.get_database_row_ids_with_view_id(&view_id).await?; + let row_oids = row_oids + .into_iter() + .map(|oid| oid.into_inner()) + .collect::>(); + let database_metas = manager.get_all_databases_meta().await; + + let uid = user + .user_id() + .map_err(|e| e.with_context("unable to get the uid: {}"))?; + + // get the collab db + let collab_db = user + .collab_db(uid) + .map_err(|e| e.with_context("unable to get the collab"))?; + let collab_db = collab_db.upgrade().ok_or_else(|| { + FlowyError::internal().with_context( + "The collab db has been dropped, indicating that the user has switched to a new account", + ) + })?; + + let collab_read_txn = collab_db.read_txn(); + + // read the database collab from the db + let database_collab = load_collab_by_object_id(uid, &collab_read_txn, &oid).map_err(|e| { + FlowyError::internal().with_context(format!("load database collab failed: {}", e)) + })?; + + let database_encoded_collab = database_collab + // encode the collab and check the integrity of the collab + .encode_collab_v1(|collab| CollabType::Database.validate_require_data(collab)) + .map_err(|e| { + FlowyError::internal().with_context(format!("encode database collab failed: {}", e)) + })?; + + // read the database rows collab from the db + let database_row_collabs = load_collab_by_object_ids(uid, &collab_read_txn, &row_oids); + let database_row_encoded_collabs = database_row_collabs + .into_iter() + .map(|(oid, collab)| { + // encode the collab and check the integrity of the collab + let encoded_collab = collab + .encode_collab_v1(|collab| CollabType::DatabaseRow.validate_require_data(collab)) + .map_err(|e| { + FlowyError::internal() + .with_context(format!("encode database row collab failed: {}", e)) + })?; + Ok((oid, encoded_collab)) + }) + .collect::, FlowyError>>()?; + + // get the relation info from the database meta + let database_relations = database_metas + .into_iter() + .filter_map(|meta| { + let linked_views = meta.linked_views.into_iter().next()?; + Some((meta.database_id, linked_views)) + }) + .collect::>(); + + Ok(EncodedCollabWrapper::Database(DatabaseEncodedCollab { + database_encoded_collab, + database_row_encoded_collabs, + database_relations, + })) + }) } fn duplicate_view(&self, view_id: &str) -> FutureResult { @@ -567,13 +673,4 @@ impl FolderOperationHandler for ChatFolderOperation { ) -> FutureResult<(), FlowyError> { FutureResult::new(async move { Err(FlowyError::not_support()) }) } - - fn encoded_collab_v1( - &self, - _view_id: &str, - _layout: ViewLayout, - ) -> FutureResult { - // Chat view doesn't support collab - FutureResult::new(async move { Err(FlowyError::not_support()) }) - } } diff --git a/frontend/rust-lib/flowy-core/src/integrate/trait_impls.rs b/frontend/rust-lib/flowy-core/src/integrate/trait_impls.rs index a197bc170869b..7b569633cad1d 100644 --- a/frontend/rust-lib/flowy-core/src/integrate/trait_impls.rs +++ b/frontend/rust-lib/flowy-core/src/integrate/trait_impls.rs @@ -32,7 +32,7 @@ use flowy_error::{FlowyError, FlowyResult}; use flowy_folder_pub::cloud::{ FolderCloudService, FolderCollabParams, FolderData, FolderSnapshot, Workspace, WorkspaceRecord, }; -use flowy_folder_pub::entities::{PublishInfoResponse, PublishViewPayload}; +use flowy_folder_pub::entities::{PublishInfoResponse, PublishPayload}; use flowy_server_pub::af_cloud_config::AFCloudConfiguration; use flowy_server_pub::supabase_config::SupabaseConfiguration; use flowy_storage_pub::cloud::{ObjectIdentity, ObjectValue, StorageCloudService}; @@ -307,7 +307,7 @@ impl FolderCloudService for ServerProvider { fn publish_view( &self, workspace_id: &str, - payload: Vec, + payload: Vec, ) -> FutureResult<(), Error> { let workspace_id = workspace_id.to_string(); let server = self.get_server(); diff --git a/frontend/rust-lib/flowy-database2/src/manager.rs b/frontend/rust-lib/flowy-database2/src/manager.rs index c7d941f4cbf83..5d9c6a8bc7dec 100644 --- a/frontend/rust-lib/flowy-database2/src/manager.rs +++ b/frontend/rust-lib/flowy-database2/src/manager.rs @@ -169,6 +169,7 @@ impl DatabaseManager { })?; let lock_guard = database_collab.lock(); + Ok(lock_guard.get_inline_view_id()) } @@ -206,6 +207,11 @@ impl DatabaseManager { }) } + pub async fn get_database_row_ids_with_view_id(&self, view_id: &str) -> FlowyResult> { + let database = self.get_database_with_view_id(view_id).await?; + Ok(database.get_row_ids()) + } + pub async fn get_database(&self, database_id: &str) -> FlowyResult> { if let Some(editor) = self.editors.lock().await.get(database_id).cloned() { return Ok(editor); diff --git a/frontend/rust-lib/flowy-database2/src/services/database/database_editor.rs b/frontend/rust-lib/flowy-database2/src/services/database/database_editor.rs index 48f16f20bdac8..40ce8db2439f4 100644 --- a/frontend/rust-lib/flowy-database2/src/services/database/database_editor.rs +++ b/frontend/rust-lib/flowy-database2/src/services/database/database_editor.rs @@ -93,6 +93,17 @@ impl DatabaseEditor { self.database_views.close_view(view_id).await; } + pub fn get_row_ids(&self) -> Vec { + self + .database + .lock() + .block + .rows + .iter() + .map(|entry| entry.key().clone()) + .collect() + } + pub async fn num_views(&self) -> usize { self.database_views.num_editors().await } diff --git a/frontend/rust-lib/flowy-document/src/event_handler.rs b/frontend/rust-lib/flowy-document/src/event_handler.rs index 580ba85587f8c..eeb7e13f57d32 100644 --- a/frontend/rust-lib/flowy-document/src/event_handler.rs +++ b/frontend/rust-lib/flowy-document/src/event_handler.rs @@ -42,7 +42,7 @@ pub(crate) async fn get_encode_collab_handler( let manager = upgrade_document(manager)?; let params: OpenDocumentParams = data.into_inner().try_into()?; let doc_id = params.document_id; - let state = manager.encode_collab(&doc_id).await?; + let state = manager.get_encoded_collab_with_view_id(&doc_id).await?; data_result_ok(EncodedCollabPB { state_vector: Vec::from(state.state_vector), doc_state: Vec::from(state.doc_state), diff --git a/frontend/rust-lib/flowy-document/src/manager.rs b/frontend/rust-lib/flowy-document/src/manager.rs index 9b13129b60907..813270edcc597 100644 --- a/frontend/rust-lib/flowy-document/src/manager.rs +++ b/frontend/rust-lib/flowy-document/src/manager.rs @@ -11,7 +11,6 @@ use collab_document::document_awareness::DocumentAwarenessState; use collab_document::document_awareness::DocumentAwarenessUser; use collab_document::document_data::default_document_data; use collab_entity::CollabType; -use collab_plugins::local_storage::kv::PersistenceError; use collab_plugins::CollabKVDB; use dashmap::DashMap; use lib_infra::util::timestamp; @@ -76,7 +75,7 @@ impl DocumentManager { } /// Get the encoded collab of the document. - pub async fn encode_collab(&self, doc_id: &str) -> FlowyResult { + pub async fn get_encoded_collab_with_view_id(&self, doc_id: &str) -> FlowyResult { let doc_state = DataSource::Disk; let uid = self.user_service.user_id()?; let collab = self @@ -85,7 +84,7 @@ impl DocumentManager { let collab = collab.lock(); collab - .encode_collab_v1(|_| Ok::<(), PersistenceError>(())) + .encode_collab_v1(|collab| CollabType::Document.validate_require_data(collab)) .map_err(internal_error) } diff --git a/frontend/rust-lib/flowy-folder-pub/src/cloud.rs b/frontend/rust-lib/flowy-folder-pub/src/cloud.rs index a017215f8891b..c34de4a3af2e6 100644 --- a/frontend/rust-lib/flowy-folder-pub/src/cloud.rs +++ b/frontend/rust-lib/flowy-folder-pub/src/cloud.rs @@ -3,7 +3,7 @@ use collab_entity::CollabType; pub use collab_folder::{Folder, FolderData, Workspace}; use uuid::Uuid; -use crate::entities::{PublishInfoResponse, PublishViewPayload}; +use crate::entities::{PublishInfoResponse, PublishPayload}; use lib_infra::future::FutureResult; /// [FolderCloudService] represents the cloud service for folder. @@ -49,7 +49,7 @@ pub trait FolderCloudService: Send + Sync + 'static { fn publish_view( &self, workspace_id: &str, - payload: Vec, + payload: Vec, ) -> FutureResult<(), Error>; fn unpublish_views(&self, workspace_id: &str, view_ids: Vec) -> FutureResult<(), Error>; diff --git a/frontend/rust-lib/flowy-folder-pub/src/entities.rs b/frontend/rust-lib/flowy-folder-pub/src/entities.rs index e67a780fbaabd..9f6624b793da0 100644 --- a/frontend/rust-lib/flowy-folder-pub/src/entities.rs +++ b/frontend/rust-lib/flowy-folder-pub/src/entities.rs @@ -1,6 +1,6 @@ use crate::folder_builder::ParentChildViews; use collab_folder::{ViewIcon, ViewLayout}; -use serde::Serialize; +use serde::{Deserialize, Serialize}; use std::collections::HashMap; pub enum ImportData { @@ -70,17 +70,51 @@ pub struct PublishViewMeta { pub publish_name: String, } +#[derive(Debug, Clone, Serialize, Deserialize, Default, Eq, PartialEq)] +pub struct PublishDatabaseData { + /// The encoded collab data for the database itself + pub database_collab: Vec, + + /// The encoded collab data for the database rows + /// Use the row_id as the key + pub database_row_collabs: HashMap>, + + /// The encoded collab data for the documents inside the database rows + /// It's not used for now + pub database_row_document_collabs: HashMap>, + + /// Visible view ids + pub visible_database_view_ids: Vec, + + /// Relation view id map + pub database_relations: HashMap, +} + #[derive(Clone, Debug, Eq, PartialEq)] -pub struct PublishViewPayload { +pub struct PublishDocumentPayload { pub meta: PublishViewMeta, - /// The doc_state of the encoded collab. + + /// The encoded collab data for the document pub data: Vec, } +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct PublishDatabasePayload { + pub meta: PublishViewMeta, + pub data: PublishDatabaseData, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum PublishPayload { + Document(PublishDocumentPayload), + Database(PublishDatabasePayload), + Unknown, +} + #[derive(Clone, Debug, Eq, PartialEq)] pub struct PublishInfoResponse { pub view_id: String, - /// one part of publish url: /{namespace}/{publish_name} - pub publish_name: String, + /// One part of publish url: /{namespace}/{publish_name} pub namespace: Option, + pub publish_name: String, } diff --git a/frontend/rust-lib/flowy-folder/src/entities/publish.rs b/frontend/rust-lib/flowy-folder/src/entities/publish.rs index 740d58c3668fe..4915fc745b70c 100644 --- a/frontend/rust-lib/flowy-folder/src/entities/publish.rs +++ b/frontend/rust-lib/flowy-folder/src/entities/publish.rs @@ -1,12 +1,18 @@ use flowy_derive::ProtoBuf; use flowy_folder_pub::entities::PublishInfoResponse; +use super::RepeatedViewIdPB; + #[derive(Default, ProtoBuf)] pub struct PublishViewParamsPB { #[pb(index = 1)] pub view_id: String, + #[pb(index = 2, one_of)] pub publish_name: Option, + + #[pb(index = 3, one_of)] + pub selected_view_ids: Option, } #[derive(Default, ProtoBuf)] diff --git a/frontend/rust-lib/flowy-folder/src/event_handler.rs b/frontend/rust-lib/flowy-folder/src/event_handler.rs index f2cd18c0b7d8c..e3426db8a8614 100644 --- a/frontend/rust-lib/flowy-folder/src/event_handler.rs +++ b/frontend/rust-lib/flowy-folder/src/event_handler.rs @@ -411,8 +411,13 @@ pub(crate) async fn publish_view_handler( ) -> Result<(), FlowyError> { let folder = upgrade_folder(folder)?; let params = data.into_inner(); + let selected_view_ids = params.selected_view_ids.map(|ids| ids.items); folder - .publish_view(params.view_id.as_str(), params.publish_name) + .publish_view( + params.view_id.as_str(), + params.publish_name, + selected_view_ids, + ) .await?; Ok(()) } @@ -428,7 +433,7 @@ pub(crate) async fn unpublish_views_handler( Ok(()) } -#[tracing::instrument(level = "debug", skip(data, folder), err)] +#[tracing::instrument(level = "debug", skip(data, folder))] pub(crate) async fn get_publish_info_handler( data: AFPluginData, folder: AFPluginState>, diff --git a/frontend/rust-lib/flowy-folder/src/manager.rs b/frontend/rust-lib/flowy-folder/src/manager.rs index 3709fa4aade95..b1dc6c0f00970 100644 --- a/frontend/rust-lib/flowy-folder/src/manager.rs +++ b/frontend/rust-lib/flowy-folder/src/manager.rs @@ -17,7 +17,9 @@ use crate::share::{ImportParams, ImportValue}; use crate::util::{ folder_not_init_error, insert_parent_child_views, workspace_data_not_sync_error, }; -use crate::view_operation::{create_view, FolderOperationHandler, FolderOperationHandlers}; +use crate::view_operation::{ + create_view, EncodedCollabWrapper, FolderOperationHandler, FolderOperationHandlers, +}; use collab::core::collab::{DataSource, MutexCollab}; use collab_entity::{CollabType, EncodedCollab}; use collab_folder::error::FolderError; @@ -30,13 +32,15 @@ use collab_integrate::CollabKVDB; use flowy_error::{internal_error, ErrorCode, FlowyError, FlowyResult}; use flowy_folder_pub::cloud::{gen_view_id, FolderCloudService, FolderCollabParams}; use flowy_folder_pub::entities::{ - PublishInfoResponse, PublishViewInfo, PublishViewMeta, PublishViewMetaData, PublishViewPayload, + PublishDatabaseData, PublishDatabasePayload, PublishDocumentPayload, PublishInfoResponse, + PublishPayload, PublishViewInfo, PublishViewMeta, PublishViewMetaData, }; use flowy_folder_pub::folder_builder::ParentChildViews; use flowy_search_pub::entities::FolderIndexManager; use flowy_sqlite::kv::KVStorePreferences; use futures::future; use parking_lot::RwLock; +use std::collections::HashMap; use std::fmt::{Display, Formatter}; use std::ops::Deref; use std::sync::{Arc, Weak}; @@ -1029,28 +1033,50 @@ impl FolderManager { Ok(()) } - /// Publish the view with the given view id. - /// [publish_name] is one part of the URL of the published view. if it is None, the default publish name will be used. The default publish name is generated by the view id and view name. + /// Publishes a view identified by the given `view_id`. + /// + /// If `publish_name` is `None`, a default name will be generated using the view name and view id. #[tracing::instrument(level = "debug", skip(self), err)] - pub async fn publish_view(&self, view_id: &str, publish_name: Option) -> FlowyResult<()> { + pub async fn publish_view( + &self, + view_id: &str, + publish_name: Option, + selected_view_ids: Option>, + ) -> FlowyResult<()> { let view = self .with_folder(|| None, |folder| folder.views.get_view(view_id)) - .ok_or_else(|| FlowyError::record_not_found().with_context("Can't find the view"))?; - - let layout = view.layout.clone(); + .ok_or_else(|| { + FlowyError::record_not_found() + .with_context(format!("Can't find the view with ID: {}", view_id)) + })?; - if layout != ViewLayout::Document { + if view.layout == ViewLayout::Chat { return Err(FlowyError::new( ErrorCode::NotSupportYet, - "Only document view can be published".to_string(), + "The chat view is not supported to publish.".to_string(), )); } - // Get the view payload and its child views recursively + // Retrieve the view payload and its child views recursively let payload = self - .get_batch_publish_payload(view_id, publish_name, Some(false)) + .get_batch_publish_payload(view_id, publish_name, false) .await?; + // set the selected view ids to the payload + let payload = if let Some(selected_view_ids) = selected_view_ids { + payload + .into_iter() + .map(|mut p| { + if let PublishPayload::Database(p) = &mut p { + p.data.visible_database_view_ids = selected_view_ids.clone(); + } + p + }) + .collect::>() + } else { + payload + }; + let workspace_id = self.user.workspace_id()?; self .cloud_service @@ -1101,14 +1127,18 @@ impl FolderManager { Ok(namespace) } - /// Get the publishing payload of the view with the given view id. - /// The publishing payload contains the view data and its child views(not recursively). + /// Retrieves the publishing payload for a specified view and optionally its child views. + /// + /// # Arguments + /// * `view_id` - The ID of the view to publish. + /// * `publish_name` - Optional name for the published view. + /// * `include_children` - Flag to include child views in the payload. pub async fn get_batch_publish_payload( &self, view_id: &str, publish_name: Option, - include_children: Option, - ) -> FlowyResult> { + include_children: bool, + ) -> FlowyResult> { let mut stack = vec![view_id.to_string()]; let mut payloads = Vec::new(); @@ -1118,12 +1148,12 @@ impl FolderManager { Err(_) => continue, }; - // Only document view can be published - let layout = if view.layout == ViewLayoutPB::Document { - ViewLayout::Document - } else { + // Skip the chat view + if view.layout == ViewLayoutPB::Chat { continue; - }; + } + + let layout: ViewLayout = view.layout.into(); // Only support set the publish_name for the current view, not for the child views let publish_name = if current_view_id == view_id { @@ -1132,19 +1162,16 @@ impl FolderManager { None }; - let payload = self + if let Ok(payload) = self .get_publish_payload(¤t_view_id, publish_name, layout) - .await; - - if let Ok(payload) = payload { + .await + { payloads.push(payload); } - if include_children.unwrap_or(false) { + if include_children { // Add the child views to the stack - for child in &view.child_views { - stack.push(child.id.clone()); - } + stack.extend(view.child_views.iter().map(|child| child.id.clone())); } } @@ -1182,24 +1209,26 @@ impl FolderManager { Some(view) } + async fn get_publish_payload( &self, view_id: &str, publish_name: Option, layout: ViewLayout, - ) -> FlowyResult { - let handler = self.get_handler(&layout)?; - let encoded_collab = handler.encoded_collab_v1(view_id, layout).await?; - let view = self - .with_folder(|| None, |folder| folder.views.get_view(view_id)) - .ok_or_else(|| FlowyError::record_not_found().with_context("Can't find the view"))?; + ) -> FlowyResult { + let handler: Arc = self.get_handler(&layout)?; + let encoded_collab_wrapper: EncodedCollabWrapper = handler + .get_encoded_collab_v1_from_disk(self.user.clone(), view_id) + .await?; + let view = self.get_view_pb(view_id).await?; + let publish_name = publish_name.unwrap_or_else(|| generate_publish_name(&view.id, &view.name)); let child_views = self .build_publish_views(view_id) .await - .map(|v| v.child_views.map_or(vec![], |c| c)) - .map_or(vec![], |c| c); + .and_then(|v| v.child_views) + .unwrap_or_default(); let ancestor_views = self .get_view_ancestors_pb(view_id) @@ -1208,9 +1237,8 @@ impl FolderManager { .map(view_pb_to_publish_view) .collect::>(); - let view_pb = self.get_view_pb(view_id).await?; let metadata = PublishViewMetaData { - view: view_pb_to_publish_view(&view_pb), + view: view_pb_to_publish_view(&view), child_views, ancestor_views, }; @@ -1220,8 +1248,32 @@ impl FolderManager { metadata, }; - let data = Vec::from(encoded_collab.doc_state); - Ok(PublishViewPayload { meta, data }) + let payload = match encoded_collab_wrapper { + EncodedCollabWrapper::Database(v) => { + let database_collab = v.database_encoded_collab.doc_state.to_vec(); + let database_relations = v.database_relations; + let database_row_collabs = v + .database_row_encoded_collabs + .into_iter() + .map(|v| (v.0, v.1.doc_state.to_vec())) // Convert to HashMap + .collect::>>(); + + let data = PublishDatabaseData { + database_collab, + database_row_collabs, + database_relations, + ..Default::default() + }; + PublishPayload::Database(PublishDatabasePayload { meta, data }) + }, + EncodedCollabWrapper::Document(v) => { + let data = v.document_encoded_collab.doc_state.to_vec(); + PublishPayload::Document(PublishDocumentPayload { meta, data }) + }, + EncodedCollabWrapper::Unknown => PublishPayload::Unknown, + }; + + Ok(payload) } // Used by toggle_favorites to send notification to frontend, after the favorite status of view has been changed.It sends two distinct notifications: one to correctly update the concerned view's is_favorite status, and another to update the list of favorites that is to be displayed. diff --git a/frontend/rust-lib/flowy-folder/src/view_operation.rs b/frontend/rust-lib/flowy-folder/src/view_operation.rs index 57edf1ba1564b..0f8df99e08bec 100644 --- a/frontend/rust-lib/flowy-folder/src/view_operation.rs +++ b/frontend/rust-lib/flowy-folder/src/view_operation.rs @@ -14,10 +14,30 @@ use lib_infra::future::FutureResult; use lib_infra::util::timestamp; use crate::entities::{CreateViewParams, ViewLayoutPB}; +use crate::manager::FolderUser; use crate::share::ImportType; pub type ViewData = Bytes; +#[derive(Debug, Clone)] +pub enum EncodedCollabWrapper { + Document(DocumentEncodedCollab), + Database(DatabaseEncodedCollab), + Unknown, +} + +#[derive(Debug, Clone)] +pub struct DocumentEncodedCollab { + pub document_encoded_collab: EncodedCollab, +} + +#[derive(Debug, Clone)] +pub struct DatabaseEncodedCollab { + pub database_encoded_collab: EncodedCollab, + pub database_row_encoded_collabs: HashMap, + pub database_relations: HashMap, +} + /// 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. @@ -45,11 +65,14 @@ pub trait FolderOperationHandler { /// Returns the [ViewData] that can be used to create the same view. fn duplicate_view(&self, view_id: &str) -> FutureResult; - fn encoded_collab_v1( + /// get the encoded collab data from the disk. + fn get_encoded_collab_v1_from_disk( &self, - view_id: &str, - layout: ViewLayout, - ) -> FutureResult; + _user: Arc, + _view_id: &str, + ) -> FutureResult { + FutureResult::new(async move { Err(FlowyError::not_support()) }) + } /// Create a view with the data. /// diff --git a/frontend/rust-lib/flowy-server/src/af_cloud/impls/folder.rs b/frontend/rust-lib/flowy-server/src/af_cloud/impls/folder.rs index 7468c163fd2dc..c8296bd01e690 100644 --- a/frontend/rust-lib/flowy-server/src/af_cloud/impls/folder.rs +++ b/frontend/rust-lib/flowy-server/src/af_cloud/impls/folder.rs @@ -7,6 +7,7 @@ use collab::core::collab::DataSource; use collab::core::origin::CollabOrigin; use collab_entity::CollabType; use collab_folder::RepeatedViewIdentifier; +use serde_json::to_vec; use std::sync::Arc; use tracing::instrument; use uuid::Uuid; @@ -16,7 +17,7 @@ use flowy_folder_pub::cloud::{ Folder, FolderCloudService, FolderCollabParams, FolderData, FolderSnapshot, Workspace, WorkspaceRecord, }; -use flowy_folder_pub::entities::{PublishInfoResponse, PublishViewPayload}; +use flowy_folder_pub::entities::{PublishInfoResponse, PublishPayload}; use lib_infra::future::FutureResult; use crate::af_cloud::define::ServerUser; @@ -187,22 +188,31 @@ where fn publish_view( &self, workspace_id: &str, - payload: Vec, + payload: Vec, ) -> FutureResult<(), Error> { let workspace_id = workspace_id.to_string(); let try_get_client = self.inner.try_get_client(); - FutureResult::new(async move { - let params = payload - .into_iter() - .map(|object| PublishCollabItem { + let params = payload + .into_iter() + .filter_map(|object| { + let (meta, data) = match object { + PublishPayload::Document(payload) => (payload.meta, payload.data), + PublishPayload::Database(payload) => { + (payload.meta, to_vec(&payload.data).unwrap_or_default()) + }, + PublishPayload::Unknown => return None, + }; + Some(PublishCollabItem { meta: PublishCollabMetadata { - view_id: Uuid::parse_str(object.meta.view_id.as_str()).unwrap_or(Uuid::nil()), - publish_name: object.meta.publish_name, - metadata: object.meta.metadata, + view_id: Uuid::parse_str(&meta.view_id).unwrap_or(Uuid::nil()), + publish_name: meta.publish_name, + metadata: meta.metadata, }, - data: object.data, + data, }) - .collect::>(); + }) + .collect::>(); + FutureResult::new(async move { try_get_client? .publish_collabs(&workspace_id, params) .await diff --git a/frontend/rust-lib/flowy-server/src/local_server/impls/folder.rs b/frontend/rust-lib/flowy-server/src/local_server/impls/folder.rs index 5c3a5464ed0de..3451212f6f3c4 100644 --- a/frontend/rust-lib/flowy-server/src/local_server/impls/folder.rs +++ b/frontend/rust-lib/flowy-server/src/local_server/impls/folder.rs @@ -7,7 +7,7 @@ use flowy_folder_pub::cloud::{ gen_workspace_id, FolderCloudService, FolderCollabParams, FolderData, FolderSnapshot, Workspace, WorkspaceRecord, }; -use flowy_folder_pub::entities::{PublishInfoResponse, PublishViewPayload}; +use flowy_folder_pub::entities::{PublishInfoResponse, PublishPayload}; use lib_infra::future::FutureResult; use crate::local_server::LocalServerDB; @@ -82,7 +82,7 @@ impl FolderCloudService for LocalServerFolderCloudServiceImpl { fn publish_view( &self, _workspace_id: &str, - _payload: Vec, + _payload: Vec, ) -> FutureResult<(), Error> { FutureResult::new(async { Err(anyhow!("Local server doesn't support publish view")) }) } diff --git a/frontend/rust-lib/flowy-server/src/supabase/api/folder.rs b/frontend/rust-lib/flowy-server/src/supabase/api/folder.rs index dc79ce60af59c..647fb7a5e7f8c 100644 --- a/frontend/rust-lib/flowy-server/src/supabase/api/folder.rs +++ b/frontend/rust-lib/flowy-server/src/supabase/api/folder.rs @@ -13,7 +13,7 @@ use flowy_folder_pub::cloud::{ gen_workspace_id, Folder, FolderCloudService, FolderCollabParams, FolderData, FolderSnapshot, Workspace, WorkspaceRecord, }; -use flowy_folder_pub::entities::{PublishInfoResponse, PublishViewPayload}; +use flowy_folder_pub::entities::{PublishInfoResponse, PublishPayload}; use lib_dispatch::prelude::af_spawn; use lib_infra::future::FutureResult; use lib_infra::util::timestamp; @@ -176,7 +176,7 @@ where fn publish_view( &self, _workspace_id: &str, - _payload: Vec, + _payload: Vec, ) -> FutureResult<(), Error> { FutureResult::new(async { Err(anyhow!("supabase server doesn't support publish view")) }) } diff --git a/frontend/rust-lib/flowy-user/src/services/data_import/appflowy_data_import.rs b/frontend/rust-lib/flowy-user/src/services/data_import/appflowy_data_import.rs index 41d9d25a34c24..6f7271658e5cb 100644 --- a/frontend/rust-lib/flowy-user/src/services/data_import/appflowy_data_import.rs +++ b/frontend/rust-lib/flowy-user/src/services/data_import/appflowy_data_import.rs @@ -1,6 +1,6 @@ use crate::migrations::session_migration::migrate_session_with_user_uuid; -use crate::services::data_import::importer::load_collab_by_oid; +use crate::services::data_import::importer::load_collab_by_object_ids; use crate::services::db::UserDBPath; use crate::services::entities::UserPaths; use crate::services::sqlite_sql::user_sql::select_user_profile; @@ -200,7 +200,7 @@ pub(crate) fn generate_import_data( all_imported_object_ids.retain(|id| !database_view_ids.contains(id)); // 3. load imported collab objects data. - let imported_collab_by_oid = load_collab_by_oid( + let imported_collab_by_oid = load_collab_by_object_ids( imported_session.user_id, &imported_collab_read_txn, &all_imported_object_ids, @@ -914,7 +914,7 @@ where R: CollabKVAction<'a>, PersistenceError: From, { - load_collab_by_oid(uid, collab_read, object_ids) + load_collab_by_object_ids(uid, collab_read, object_ids) .into_iter() .filter_map(|(oid, collab)| { collab diff --git a/frontend/rust-lib/flowy-user/src/services/data_import/importer.rs b/frontend/rust-lib/flowy-user/src/services/data_import/importer.rs index b1dee99774b4d..5604024be3c88 100644 --- a/frontend/rust-lib/flowy-user/src/services/data_import/importer.rs +++ b/frontend/rust-lib/flowy-user/src/services/data_import/importer.rs @@ -3,8 +3,9 @@ use collab_integrate::{CollabKVAction, PersistenceError}; use std::collections::HashMap; use tracing::instrument; +/// This function loads collab objects by their object_ids. #[instrument(level = "debug", skip_all)] -pub fn load_collab_by_oid<'a, R>( +pub fn load_collab_by_object_ids<'a, R>( uid: i64, collab_read_txn: &R, object_ids: &[String], @@ -14,17 +15,32 @@ where PersistenceError: From, { let mut collab_by_oid = HashMap::new(); + for object_id in object_ids { - let collab = Collab::new(uid, object_id, "phantom", vec![], false); - match collab - .with_origin_transact_mut(|txn| collab_read_txn.load_doc_with_txn(uid, &object_id, txn)) - { - Ok(_) => { + match load_collab_by_object_id(uid, collab_read_txn, object_id) { + Ok(collab) => { collab_by_oid.insert(object_id.clone(), collab); }, - Err(err) => tracing::error!("🔴import collab:{} failed: {:?} ", object_id, err), + Err(err) => tracing::error!("🔴load collab: {} failed: {:?} ", object_id, err), } } collab_by_oid } + +/// This function loads single collab object by its object_id. +#[instrument(level = "debug", skip_all)] +pub fn load_collab_by_object_id<'a, R>( + uid: i64, + collab_read_txn: &R, + object_id: &String, +) -> Result +where + R: CollabKVAction<'a>, + PersistenceError: From, +{ + let collab = Collab::new(uid, object_id, "phantom", vec![], false); + collab + .with_origin_transact_mut(|txn| collab_read_txn.load_doc_with_txn(uid, object_id, txn)) + .map(|_| collab) +} diff --git a/frontend/rust-lib/flowy-user/src/services/data_import/mod.rs b/frontend/rust-lib/flowy-user/src/services/data_import/mod.rs index 2e5ddf96030f6..9fec671adec2c 100644 --- a/frontend/rust-lib/flowy-user/src/services/data_import/mod.rs +++ b/frontend/rust-lib/flowy-user/src/services/data_import/mod.rs @@ -2,4 +2,5 @@ mod appflowy_data_import; pub use appflowy_data_import::*; pub(crate) mod importer; -pub use importer::load_collab_by_oid; +pub use importer::load_collab_by_object_id; +pub use importer::load_collab_by_object_ids;