feat: optimize sync error page (#6082)

This commit is contained in:
Lucas.Xu 2024-08-28 18:53:16 +08:00 committed by GitHub
parent 9a295daf99
commit 9ee8cc6a7b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
32 changed files with 438 additions and 72 deletions

View File

@ -1,7 +1,7 @@
import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_page_block.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_page_block.dart';
import 'package:appflowy/plugins/document/presentation/sync_error_page.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';
import 'package:flowy_infra/uuid.dart'; import 'package:flowy_infra/uuid.dart';
import 'package:flowy_infra_ui/widget/error_page.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart'; import 'package:integration_test/integration_test.dart';
@ -92,7 +92,7 @@ void main() {
); );
expect(finder, findsOneWidget); expect(finder, findsOneWidget);
await tester.tapButton(finder); await tester.tapButton(finder);
expect(find.byType(FlowyErrorPage), findsOneWidget); expect(find.byType(SyncErrorPage), findsOneWidget);
}); });
}); });
} }

View File

@ -1,6 +1,6 @@
import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/base/gesture.dart'; import 'package:appflowy/mobile/presentation/base/animated_gesture.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart'; import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
import 'package:appflowy/mobile/presentation/home/workspaces/workspace_menu_bottom_sheet.dart'; import 'package:appflowy/mobile/presentation/home/workspaces/workspace_menu_bottom_sheet.dart';
import 'package:appflowy/plugins/base/emoji/emoji_picker_screen.dart'; import 'package:appflowy/plugins/base/emoji/emoji_picker_screen.dart';
@ -109,6 +109,7 @@ class _MobileWorkspace extends StatelessWidget {
return const SizedBox.shrink(); return const SizedBox.shrink();
} }
return AnimatedGestureDetector( return AnimatedGestureDetector(
scaleFactor: 0.99,
alignment: Alignment.centerLeft, alignment: Alignment.centerLeft,
onTapUp: () { onTapUp: () {
context.read<UserWorkspaceBloc>().add( context.read<UserWorkspaceBloc>().add(

View File

@ -5,7 +5,7 @@ import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/application/mobile_router.dart'; import 'package:appflowy/mobile/application/mobile_router.dart';
import 'package:appflowy/mobile/application/page_style/document_page_style_bloc.dart'; import 'package:appflowy/mobile/application/page_style/document_page_style_bloc.dart';
import 'package:appflowy/mobile/application/recent/recent_view_bloc.dart'; import 'package:appflowy/mobile/application/recent/recent_view_bloc.dart';
import 'package:appflowy/mobile/presentation/base/gesture.dart'; import 'package:appflowy/mobile/presentation/base/animated_gesture.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart'; import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
import 'package:appflowy/shared/appflowy_network_image.dart'; import 'package:appflowy/shared/appflowy_network_image.dart';

View File

@ -1,6 +1,6 @@
import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/base/gesture.dart'; import 'package:appflowy/mobile/presentation/base/animated_gesture.dart';
import 'package:appflowy/mobile/presentation/home/tab/mobile_space_tab.dart'; import 'package:appflowy/mobile/presentation/home/tab/mobile_space_tab.dart';
import 'package:appflowy/util/theme_extension.dart'; import 'package:appflowy/util/theme_extension.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';

View File

@ -1,5 +1,5 @@
import 'package:appflowy/mobile/application/notification/notification_reminder_bloc.dart'; import 'package:appflowy/mobile/application/notification/notification_reminder_bloc.dart';
import 'package:appflowy/mobile/presentation/base/gesture.dart'; import 'package:appflowy/mobile/presentation/base/animated_gesture.dart';
import 'package:appflowy/mobile/presentation/notifications/mobile_notifications_screen.dart'; import 'package:appflowy/mobile/presentation/notifications/mobile_notifications_screen.dart';
import 'package:appflowy/mobile/presentation/notifications/widgets/widgets.dart'; import 'package:appflowy/mobile/presentation/notifications/widgets/widgets.dart';
import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart'; import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart';

View File

@ -1,6 +1,6 @@
import 'package:appflowy/mobile/application/mobile_router.dart'; import 'package:appflowy/mobile/application/mobile_router.dart';
import 'package:appflowy/mobile/application/notification/notification_reminder_bloc.dart'; import 'package:appflowy/mobile/application/notification/notification_reminder_bloc.dart';
import 'package:appflowy/mobile/presentation/base/gesture.dart'; import 'package:appflowy/mobile/presentation/base/animated_gesture.dart';
import 'package:appflowy/mobile/presentation/notifications/widgets/widgets.dart'; import 'package:appflowy/mobile/presentation/notifications/widgets/widgets.dart';
import 'package:appflowy/user/application/reminder/reminder_bloc.dart'; import 'package:appflowy/user/application/reminder/reminder_bloc.dart';
import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart'; import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart';

View File

@ -1,4 +1,3 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/application/page_style/document_page_style_bloc.dart'; import 'package:appflowy/mobile/application/page_style/document_page_style_bloc.dart';
import 'package:appflowy/plugins/document/application/document_bloc.dart'; import 'package:appflowy/plugins/document/application/document_bloc.dart';
import 'package:appflowy/plugins/document/presentation/banner.dart'; import 'package:appflowy/plugins/document/presentation/banner.dart';
@ -12,6 +11,7 @@ import 'package:appflowy/plugins/document/presentation/editor_plugins/image/cust
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/multi_image_block_component/multi_image_block_component.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/image/multi_image_block_component/multi_image_block_component.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
import 'package:appflowy/plugins/document/presentation/editor_style.dart'; import 'package:appflowy/plugins/document/presentation/editor_style.dart';
import 'package:appflowy/plugins/document/presentation/sync_error_page.dart';
import 'package:appflowy/shared/patterns/common_patterns.dart'; import 'package:appflowy/shared/patterns/common_patterns.dart';
import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/workspace/application/action_navigation/action_navigation_bloc.dart'; import 'package:appflowy/workspace/application/action_navigation/action_navigation_bloc.dart';
@ -22,8 +22,6 @@ import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:appflowy_editor/appflowy_editor.dart' hide Log; import 'package:appflowy_editor/appflowy_editor.dart' hide Log;
import 'package:cross_file/cross_file.dart'; import 'package:cross_file/cross_file.dart';
import 'package:desktop_drop/desktop_drop.dart'; import 'package:desktop_drop/desktop_drop.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/widget/error_page.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
@ -115,9 +113,10 @@ class _DocumentPageState extends State<DocumentPage>
final error = state.error; final error = state.error;
if (error != null || editorState == null) { if (error != null || editorState == null) {
Log.error(error); Log.error(error);
return FlowyErrorPage.message( return Center(
error.toString(), child: SyncErrorPage(
howToFix: LocaleKeys.errorDialog_howToFixFallback.tr(), error: error,
),
); );
} }

View File

@ -500,7 +500,7 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
void _customizeBlockComponentBackgroundColorDecorator() { void _customizeBlockComponentBackgroundColorDecorator() {
blockComponentBackgroundColorDecorator = (Node node, String colorString) { blockComponentBackgroundColorDecorator = (Node node, String colorString) {
if (context.mounted) { if (mounted && context.mounted) {
return buildEditorCustomizedColor(context, node, colorString); return buildEditorCustomizedColor(context, node, colorString);
} }
return null; return null;

View File

@ -1,6 +1,7 @@
import 'dart:async'; import 'dart:async';
import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/mobile/presentation/base/animated_gesture.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_toolbar_theme.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_toolbar_theme.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/appflowy_mobile_toolbar.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/appflowy_mobile_toolbar.dart';
import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy_editor/appflowy_editor.dart';
@ -106,9 +107,9 @@ class _AppFlowyMobileToolbarIconItemState
final enable = widget.enable?.call() ?? true; final enable = widget.enable?.call() ?? true;
return Padding( return Padding(
padding: const EdgeInsets.symmetric(vertical: 5), padding: const EdgeInsets.symmetric(vertical: 5),
child: GestureDetector( child: AnimatedGestureDetector(
behavior: HitTestBehavior.opaque, scaleFactor: 0.95,
onTap: () { onTapUp: () {
widget.onTap(); widget.onTap();
_rebuild(); _rebuild();
}, },

View File

@ -138,7 +138,7 @@ class EditorStyleCustomizer {
fontSize: fontSize, fontSize: fontSize,
fontWeight: FontWeight.normal, fontWeight: FontWeight.normal,
color: Colors.red, color: Colors.red,
backgroundColor: theme.colorScheme.inverseSurface.withOpacity(0.8), backgroundColor: Colors.grey.withOpacity(0.3),
), ),
), ),
applyHeightToFirstAscent: true, applyHeightToFirstAscent: true,

View File

@ -0,0 +1,168 @@
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/mobile/presentation/base/animated_gesture.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:appflowy_editor/appflowy_editor.dart' show PlatformExtension;
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flowy_infra_ui/widget/spacing.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
class SyncErrorPage extends StatelessWidget {
const SyncErrorPage({
super.key,
this.error,
});
final FlowyError? error;
@override
Widget build(BuildContext context) {
if (PlatformExtension.isMobile) {
return _MobileSyncErrorPage(error: error);
} else {
return _DesktopSyncErrorPage(error: error);
}
}
}
class _MobileSyncErrorPage extends StatelessWidget {
const _MobileSyncErrorPage({
this.error,
});
final FlowyError? error;
@override
Widget build(BuildContext context) {
return AnimatedGestureDetector(
scaleFactor: 0.99,
onTapUp: () {
getIt<ClipboardService>().setPlainText(error.toString());
showToastNotification(
context,
message: LocaleKeys.message_copy_success.tr(),
bottomPadding: 0,
);
},
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
const FlowySvg(
FlowySvgs.icon_warning_xl,
blendMode: null,
),
const VSpace(16.0),
FlowyText.medium(
LocaleKeys.error_syncError.tr(),
fontSize: 15,
),
const VSpace(8.0),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 24.0),
child: FlowyText.regular(
LocaleKeys.error_syncErrorHint.tr(),
fontSize: 13,
color: Theme.of(context).hintColor,
textAlign: TextAlign.center,
maxLines: 10,
),
),
const VSpace(2.0),
FlowyText.regular(
'(${LocaleKeys.error_clickToCopy.tr()})',
fontSize: 13,
color: Theme.of(context).hintColor,
textAlign: TextAlign.center,
),
],
),
);
}
}
class _DesktopSyncErrorPage extends StatelessWidget {
const _DesktopSyncErrorPage({
this.error,
});
final FlowyError? error;
@override
Widget build(BuildContext context) {
return AnimatedGestureDetector(
scaleFactor: 0.995,
onTapUp: () {
getIt<ClipboardService>().setPlainText(error.toString());
showToastNotification(
context,
message: LocaleKeys.message_copy_success.tr(),
bottomPadding: 0,
);
},
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
const FlowySvg(
FlowySvgs.icon_warning_xl,
blendMode: null,
),
const VSpace(16.0),
FlowyText.medium(
error?.code.toString() ?? '',
fontSize: 16,
),
const VSpace(8.0),
RichText(
text: TextSpan(
children: [
TextSpan(
text: LocaleKeys.errorDialog_howToFixFallbackHint1.tr(),
style: TextStyle(
fontSize: 14,
color: Theme.of(context).hintColor,
),
),
TextSpan(
text: 'Github',
style: TextStyle(
fontSize: 14,
color: Theme.of(context).colorScheme.primary,
decoration: TextDecoration.underline,
),
recognizer: TapGestureRecognizer()
..onTap = () {
afLaunchUrlString(
'https://github.com/AppFlowy-IO/AppFlowy/issues/new?template=bug_report.yaml',
);
},
),
TextSpan(
text: LocaleKeys.errorDialog_howToFixFallbackHint2.tr(),
style: TextStyle(
fontSize: 14,
color: Theme.of(context).hintColor,
),
),
],
),
),
const VSpace(8.0),
FlowyText.regular(
'(${LocaleKeys.error_clickToCopy.tr()})',
fontSize: 14,
color: Theme.of(context).hintColor,
textAlign: TextAlign.center,
),
],
),
);
}
}

View File

@ -11,6 +11,7 @@ import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/file_picker/file_picker_service.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/flowy_infra_ui.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
@ -51,6 +52,14 @@ class ExportTab extends StatelessWidget {
svg: FlowySvgs.duplicate_s, svg: FlowySvgs.duplicate_s,
onTap: () => _exportToClipboard(context), onTap: () => _exportToClipboard(context),
), ),
if (kDebugMode) ...[
const VSpace(10),
_ExportButton(
title: 'JSON (Debug Mode)',
svg: FlowySvgs.duplicate_s,
onTap: () => _exportJSON(context),
),
],
], ],
); );
} }
@ -64,6 +73,14 @@ class ExportTab extends StatelessWidget {
svg: FlowySvgs.database_layout_m, svg: FlowySvgs.database_layout_m,
onTap: () => _exportCSV(context), onTap: () => _exportCSV(context),
), ),
if (kDebugMode) ...[
const VSpace(10),
_ExportButton(
title: 'Raw Database Data (Debug Mode)',
svg: FlowySvgs.duplicate_s,
onTap: () => _exportRawDatabaseData(context),
),
],
], ],
); );
} }
@ -100,6 +117,22 @@ class ExportTab extends StatelessWidget {
} }
} }
Future<void> _exportJSON(BuildContext context) async {
final viewName = context.read<ShareBloc>().state.viewName;
final exportPath = await getIt<FilePickerService>().saveFile(
dialogTitle: '',
fileName: '${viewName.toFileName()}.json',
);
if (context.mounted && exportPath != null) {
context.read<ShareBloc>().add(
ShareEvent.share(
ShareType.json,
exportPath,
),
);
}
}
Future<void> _exportCSV(BuildContext context) async { Future<void> _exportCSV(BuildContext context) async {
final viewName = context.read<ShareBloc>().state.viewName; final viewName = context.read<ShareBloc>().state.viewName;
final exportPath = await getIt<FilePickerService>().saveFile( final exportPath = await getIt<FilePickerService>().saveFile(
@ -116,6 +149,22 @@ class ExportTab extends StatelessWidget {
} }
} }
Future<void> _exportRawDatabaseData(BuildContext context) async {
final viewName = context.read<ShareBloc>().state.viewName;
final exportPath = await getIt<FilePickerService>().saveFile(
dialogTitle: '',
fileName: '${viewName.toFileName()}.json',
);
if (context.mounted && exportPath != null) {
context.read<ShareBloc>().add(
ShareEvent.share(
ShareType.rawDatabaseData,
exportPath,
),
);
}
}
Future<void> _exportToClipboard(BuildContext context) async { Future<void> _exportToClipboard(BuildContext context) async {
final documentExporter = DocumentExporter(context.read<ShareBloc>().view); final documentExporter = DocumentExporter(context.read<ShareBloc>().view);
final result = await documentExporter.export(DocumentExportType.markdown); final result = await documentExporter.export(DocumentExportType.markdown);

View File

@ -179,6 +179,14 @@ class ShareBloc extends Bloc<ShareEvent, ShareState> {
(s) => FlowyResult.success(s.data), (s) => FlowyResult.success(s.data),
(f) => FlowyResult.failure(f), (f) => FlowyResult.failure(f),
); );
} else if (type == ShareType.rawDatabaseData) {
final exportResult = await BackendExportService.exportDatabaseAsRawData(
view.id,
);
result = exportResult.fold(
(s) => FlowyResult.success(s.data),
(f) => FlowyResult.failure(f),
);
} else { } else {
result = await documentExporter.export(type.documentExportType); result = await documentExporter.export(type.documentExportType);
} }
@ -189,6 +197,8 @@ class ShareBloc extends Bloc<ShareEvent, ShareState> {
case ShareType.markdown: case ShareType.markdown:
case ShareType.html: case ShareType.html:
case ShareType.csv: case ShareType.csv:
case ShareType.json:
case ShareType.rawDatabaseData:
File(path).writeAsStringSync(s); File(path).writeAsStringSync(s);
return FlowyResult.success(type); return FlowyResult.success(type);
default: default:
@ -208,9 +218,11 @@ enum ShareType {
html, html,
text, text,
link, link,
json,
// only available in database // only available in database
csv; csv,
rawDatabaseData;
static List<ShareType> get unimplemented => [link]; static List<ShareType> get unimplemented => [link];
@ -222,10 +234,16 @@ enum ShareType {
return DocumentExportType.html; return DocumentExportType.html;
case ShareType.text: case ShareType.text:
return DocumentExportType.text; return DocumentExportType.text;
case ShareType.json:
return DocumentExportType.json;
case ShareType.csv: case ShareType.csv:
throw UnsupportedError('DocumentShareType.csv is not supported'); throw UnsupportedError('DocumentShareType.csv is not supported');
case ShareType.link: case ShareType.link:
throw UnsupportedError('DocumentShareType.link is not supported'); throw UnsupportedError('DocumentShareType.link is not supported');
case ShareType.rawDatabaseData:
throw UnsupportedError(
'DocumentShareType.rawDatabaseData is not supported',
);
} }
} }
} }

View File

@ -10,6 +10,7 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import 'package:appflowy/mobile/presentation/base/animated_gesture.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
@ -1524,9 +1525,10 @@ class PopupMenuButtonState<T> extends State<PopupMenuButton<T>> {
assert(debugCheckHasMaterialLocalizations(context)); assert(debugCheckHasMaterialLocalizations(context));
if (widget.child != null) { if (widget.child != null) {
return GestureDetector( return AnimatedGestureDetector(
onTap: widget.enabled ? showButtonMenu : null, scaleFactor: 0.99,
child: widget.child, onTapUp: widget.enabled ? showButtonMenu : null,
child: widget.child!,
); );
} }

View File

@ -109,7 +109,8 @@ class FlowyRunner {
[ [
// this task should be first task, for handling platform errors. // this task should be first task, for handling platform errors.
// don't catch errors in test mode // don't catch errors in test mode
if (!mode.isUnitTest) const PlatformErrorCatcherTask(), if (!mode.isUnitTest && !mode.isIntegrationTest)
const PlatformErrorCatcherTask(),
if (!mode.isUnitTest) const InitSentryTask(), if (!mode.isUnitTest) const InitSentryTask(),
// this task should be second task, for handling memory leak. // this task should be second task, for handling memory leak.
// there's a flag named _enable in memory_leak_detector.dart. If it's false, the task will be ignored. // there's a flag named _enable in memory_leak_detector.dart. If it's false, the task will be ignored.

View File

@ -1,5 +1,7 @@
import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/log.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import '../startup.dart'; import '../startup.dart';
@ -17,6 +19,23 @@ class PlatformErrorCatcherTask extends LaunchTask {
return true; return true;
}; };
} }
ErrorWidget.builder = (details) {
if (kDebugMode) {
return Container(
width: double.infinity,
height: 30,
color: Colors.red,
child: FlowyText(
'ERROR: ${details.exceptionAsString()}',
color: Colors.white,
),
);
}
// hide the error widget in release mode
return const SizedBox.shrink();
};
} }
@override @override

View File

@ -44,6 +44,7 @@ class MobileSignInScreen extends StatelessWidget {
const Spacer(flex: 2), const Spacer(flex: 2),
const Spacer(), const Spacer(),
Expanded(child: _buildSettingsButton(context)), Expanded(child: _buildSettingsButton(context)),
if (Platform.isAndroid) const Spacer(),
], ],
), ),
), ),

View File

@ -1,6 +1,6 @@
import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/base/gesture.dart'; import 'package:appflowy/mobile/presentation/base/animated_gesture.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';

View File

@ -12,4 +12,12 @@ class BackendExportService {
final payload = DatabaseViewIdPB.create()..value = viewId; final payload = DatabaseViewIdPB.create()..value = viewId;
return DatabaseEventExportCSV(payload).send(); return DatabaseEventExportCSV(payload).send();
} }
static Future<FlowyResult<DatabaseExportDataPB, FlowyError>>
exportDatabaseAsRawData(
String viewId,
) async {
final payload = DatabaseViewIdPB.create()..value = viewId;
return DatabaseEventExportRawDatabaseData(payload).send();
}
} }

View File

@ -452,6 +452,9 @@ class SpaceBloc extends Bloc<SpaceEvent, SpaceState> {
)..start( )..start(
sectionChanged: (result) async { sectionChanged: (result) async {
Log.info('did receive section views changed'); Log.info('did receive section views changed');
if (isClosed) {
return;
}
add(const SpaceEvent.didReceiveSpaceUpdate()); add(const SpaceEvent.didReceiveSpaceUpdate());
}, },
); );

View File

@ -1,17 +1,17 @@
import 'package:appflowy/shared/feature_flags.dart'; import 'package:appflowy/core/helpers/url_launcher.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/footer/sidebar_toast.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/setting_appflowy_cloud.dart';
import 'package:flutter/material.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/shared/feature_flags.dart';
import 'package:appflowy/startup/plugin/plugin.dart'; import 'package:appflowy/startup/plugin/plugin.dart';
import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart'; import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart';
import 'package:appflowy/workspace/presentation/home/home_sizes.dart';
import 'package:appflowy/workspace/presentation/home/menu/menu_shared_state.dart'; import 'package:appflowy/workspace/presentation/home/menu/menu_shared_state.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/footer/sidebar_toast.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/setting_appflowy_cloud.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart';
import 'sidebar_footer_button.dart';
class SidebarFooter extends StatelessWidget { class SidebarFooter extends StatelessWidget {
const SidebarFooter({super.key}); const SidebarFooter({super.key});
@ -26,18 +26,28 @@ class SidebarFooter extends StatelessWidget {
return const SidebarToast(); return const SidebarToast();
}, },
), ),
const Row( const SidebarTemplateButton(),
children: [ const SidebarTrashButton(),
Expanded(child: SidebarTrashButton()),
// Enable it when the widget button is ready
// SizedBox(
// height: 16,
// child: VerticalDivider(width: 1, color: Color(0x141F2329)),
// ),
// Expanded(child: SidebarWidgetButton()),
], ],
);
}
}
class SidebarTemplateButton extends StatelessWidget {
const SidebarTemplateButton({super.key});
@override
Widget build(BuildContext context) {
return SidebarFooterButton(
leftIconSize: const Size.square(24.0),
leftIcon: const Padding(
padding: EdgeInsets.all(2.0),
child: FlowySvg(
FlowySvgs.icon_template_s,
), ),
], ),
text: LocaleKeys.template_label.tr(),
onTap: () => afLaunchUrlString('https://appflowy.io/templates'),
); );
} }
} }
@ -47,20 +57,15 @@ class SidebarTrashButton extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return SizedBox( return ValueListenableBuilder(
height: HomeSizes.workspaceSectionHeight,
child: ValueListenableBuilder(
valueListenable: getIt<MenuSharedState>().notifier, valueListenable: getIt<MenuSharedState>().notifier,
builder: (context, value, child) { builder: (context, value, child) {
return FlowyButton( return SidebarFooterButton(
leftIcon: const FlowySvg(FlowySvgs.sidebar_footer_trash_m),
leftIconSize: const Size.square(24.0), leftIconSize: const Size.square(24.0),
iconPadding: 8.0, leftIcon: const FlowySvg(
margin: const EdgeInsets.all(4.0), FlowySvgs.sidebar_footer_trash_m,
text: FlowyText.regular(
LocaleKeys.trash_text.tr(),
lineHeight: 1.15,
), ),
text: LocaleKeys.trash_text.tr(),
onTap: () { onTap: () {
getIt<MenuSharedState>().latestOpenView = null; getIt<MenuSharedState>().latestOpenView = null;
getIt<TabsBloc>().add( getIt<TabsBloc>().add(
@ -71,7 +76,6 @@ class SidebarTrashButton extends StatelessWidget {
}, },
); );
}, },
),
); );
} }
} }

View File

@ -0,0 +1,39 @@
import 'package:appflowy/workspace/presentation/home/home_sizes.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
// This button style is used in
// - Trash button
// - Template button
class SidebarFooterButton extends StatelessWidget {
const SidebarFooterButton({
super.key,
required this.leftIcon,
required this.leftIconSize,
required this.text,
required this.onTap,
});
final Widget leftIcon;
final Size leftIconSize;
final String text;
final VoidCallback onTap;
@override
Widget build(BuildContext context) {
return SizedBox(
height: HomeSizes.workspaceSectionHeight,
child: FlowyButton(
leftIcon: leftIcon,
leftIconSize: leftIconSize,
iconPadding: 8.0,
margin: const EdgeInsets.all(4.0),
text: FlowyText.regular(
text,
lineHeight: 1.15,
),
onTap: onTap,
),
);
}
}

View File

@ -97,6 +97,13 @@ class _NavigatorTextFieldDialogState extends State<NavigatorTextFieldDialog> {
VSpace(Insets.xl), VSpace(Insets.xl),
OkCancelButton( OkCancelButton(
onOkPressed: () { onOkPressed: () {
if (newValue.isEmpty) {
showToastNotification(
context,
message: LocaleKeys.space_spaceNameCannotBeEmpty.tr(),
);
return;
}
widget.onConfirm(newValue, context); widget.onConfirm(newValue, context);
Navigator.of(context).pop(); Navigator.of(context).pop();
}, },

View File

@ -0,0 +1,5 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<g opacity="0.6">
<path d="M8.125 3.125H4.375C4.04348 3.125 3.72554 3.2567 3.49112 3.49112C3.2567 3.72554 3.125 4.04348 3.125 4.375V8.125C3.125 8.45652 3.2567 8.77446 3.49112 9.00888C3.72554 9.2433 4.04348 9.375 4.375 9.375H8.125C8.45652 9.375 8.77446 9.2433 9.00888 9.00888C9.2433 8.77446 9.375 8.45652 9.375 8.125V4.375C9.375 4.04348 9.2433 3.72554 9.00888 3.49112C8.77446 3.2567 8.45652 3.125 8.125 3.125ZM8.125 8.125H4.375V4.375H8.125V8.125ZM15.625 3.125H11.875C11.5435 3.125 11.2255 3.2567 10.9911 3.49112C10.7567 3.72554 10.625 4.04348 10.625 4.375V8.125C10.625 8.45652 10.7567 8.77446 10.9911 9.00888C11.2255 9.2433 11.5435 9.375 11.875 9.375H15.625C15.9565 9.375 16.2745 9.2433 16.5089 9.00888C16.7433 8.77446 16.875 8.45652 16.875 8.125V4.375C16.875 4.04348 16.7433 3.72554 16.5089 3.49112C16.2745 3.2567 15.9565 3.125 15.625 3.125ZM15.625 8.125H11.875V4.375H15.625V8.125ZM8.125 10.625H4.375C4.04348 10.625 3.72554 10.7567 3.49112 10.9911C3.2567 11.2255 3.125 11.5435 3.125 11.875V15.625C3.125 15.9565 3.2567 16.2745 3.49112 16.5089C3.72554 16.7433 4.04348 16.875 4.375 16.875H8.125C8.45652 16.875 8.77446 16.7433 9.00888 16.5089C9.2433 16.2745 9.375 15.9565 9.375 15.625V11.875C9.375 11.5435 9.2433 11.2255 9.00888 10.9911C8.77446 10.7567 8.45652 10.625 8.125 10.625ZM8.125 15.625H4.375V11.875H8.125V15.625ZM15.625 10.625H11.875C11.5435 10.625 11.2255 10.7567 10.9911 10.9911C10.7567 11.2255 10.625 11.5435 10.625 11.875V15.625C10.625 15.9565 10.7567 16.2745 10.9911 16.5089C11.2255 16.7433 11.5435 16.875 11.875 16.875H15.625C15.9565 16.875 16.2745 16.7433 16.5089 16.5089C16.7433 16.2745 16.875 15.9565 16.875 15.625V11.875C16.875 11.5435 16.7433 11.2255 16.5089 10.9911C16.2745 10.7567 15.9565 10.625 15.625 10.625ZM15.625 15.625H11.875V11.875H15.625V15.625Z" fill="#101012"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@ -0,0 +1,5 @@
<svg width="44" height="44" viewBox="0 0 44 44" fill="none" xmlns="http://www.w3.org/2000/svg">
<ellipse cx="21.9997" cy="21.6966" rx="20.1667" ry="19.8885" fill="#FF811A"/>
<path d="M22.0003 12.6562C20.9878 12.6562 20.167 13.4657 20.167 14.4643V23.5045C20.167 24.5031 20.9878 25.3126 22.0003 25.3126C23.0128 25.3126 23.8337 24.5031 23.8337 23.5045V14.4643C23.8337 13.4657 23.0128 12.6562 22.0003 12.6562Z" fill="white"/>
<path d="M22.0003 30.7367C23.0128 30.7367 23.8337 29.9272 23.8337 28.9287C23.8337 27.9301 23.0128 27.1206 22.0003 27.1206C20.9878 27.1206 20.167 27.9301 20.167 28.9287C20.167 29.9272 20.9878 30.7367 22.0003 30.7367Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 661 B

View File

@ -1897,6 +1897,8 @@
"errorDialog": { "errorDialog": {
"title": "@:appName Error", "title": "@:appName Error",
"howToFixFallback": "We're sorry for the inconvenience! Submit an issue on our GitHub page that describes your error.", "howToFixFallback": "We're sorry for the inconvenience! Submit an issue on our GitHub page that describes your error.",
"howToFixFallbackHint1": "We're sorry for the inconvenience! Submit an issue on our ",
"howToFixFallbackHint2": " page that describes your error.",
"github": "View on GitHub" "github": "View on GitHub"
}, },
"search": { "search": {
@ -2051,7 +2053,10 @@
}, },
"error": { "error": {
"weAreSorry": "We're sorry", "weAreSorry": "We're sorry",
"loadingViewError": "We're having trouble loading this view. Please check your internet connection, refresh the app, and do not hesitate to reach out to the team if the issue continues." "loadingViewError": "We're having trouble loading this view. Please check your internet connection, refresh the app, and do not hesitate to reach out to the team if the issue continues.",
"syncError": "Data is not synced from another device",
"syncErrorHint": "Please reopen this page on the device where it was last edited, then open it again on the current device.",
"clickToCopy": "Click to copy error code"
}, },
"editor": { "editor": {
"bold": "Bold", "bold": "Bold",
@ -2317,7 +2322,8 @@
"quicklySwitch": "Quickly switch to the next space", "quicklySwitch": "Quickly switch to the next space",
"duplicate": "Duplicate Space", "duplicate": "Duplicate Space",
"movePageToSpace": "Move page to space", "movePageToSpace": "Move page to space",
"switchSpace": "Switch space" "switchSpace": "Switch space",
"spaceNameCannotBeEmpty": "Space name cannot be empty"
}, },
"publish": { "publish": {
"hasNotBeenPublished": "This page hasn't been published yet", "hasNotBeenPublished": "This page hasn't been published yet",
@ -2484,7 +2490,8 @@
"addRelatedTemplate": "Add related template", "addRelatedTemplate": "Add related template",
"removeRelatedTemplate": "Remove related template", "removeRelatedTemplate": "Remove related template",
"uploadAvatar": "Upload avatar", "uploadAvatar": "Upload avatar",
"searchInCategory": "Search in {category}" "searchInCategory": "Search in {category}",
"label": "Template"
}, },
"fileDropzone": { "fileDropzone": {
"dropFile": "Click or drag file to this area to upload", "dropFile": "Click or drag file to this area to upload",

View File

@ -377,7 +377,7 @@ impl FolderOperationHandler for DatabaseFolderOperation {
} }
async fn duplicate_view(&self, view_id: &str) -> Result<Bytes, FlowyError> { async fn duplicate_view(&self, view_id: &str) -> Result<Bytes, FlowyError> {
let delta_bytes = self.0.duplicate_database(view_id).await?; let delta_bytes = self.0.get_database_json_bytes(view_id).await?;
Ok(Bytes::from(delta_bytes)) Ok(Bytes::from(delta_bytes))
} }

View File

@ -4,6 +4,9 @@ use flowy_derive::{ProtoBuf, ProtoBuf_Enum};
pub enum DatabaseExportDataType { pub enum DatabaseExportDataType {
#[default] #[default]
CSV = 0, CSV = 0,
// DatabaseData
RawDatabaseData = 1,
} }
#[derive(Debug, ProtoBuf, Default, Clone)] #[derive(Debug, ProtoBuf, Default, Clone)]

View File

@ -1027,6 +1027,20 @@ pub(crate) async fn export_csv_handler(
}) })
} }
#[tracing::instrument(level = "debug", skip_all, err)]
pub(crate) async fn export_raw_database_data_handler(
data: AFPluginData<DatabaseViewIdPB>,
manager: AFPluginState<Weak<DatabaseManager>>,
) -> DataResult<DatabaseExportDataPB, FlowyError> {
let manager = upgrade_manager(manager)?;
let view_id = data.into_inner().value;
let data = manager.get_database_json_string(&view_id).await?;
data_result_ok(DatabaseExportDataPB {
export_type: DatabaseExportDataType::RawDatabaseData,
data,
})
}
#[tracing::instrument(level = "debug", skip_all, err)] #[tracing::instrument(level = "debug", skip_all, err)]
pub(crate) async fn get_snapshots_handler( pub(crate) async fn get_snapshots_handler(
data: AFPluginData<DatabaseViewIdPB>, data: AFPluginData<DatabaseViewIdPB>,

View File

@ -77,6 +77,7 @@ pub fn init(database_manager: Weak<DatabaseManager>) -> AFPlugin {
.event(DatabaseEvent::CreateDatabaseView, create_database_view) .event(DatabaseEvent::CreateDatabaseView, create_database_view)
// Export // Export
.event(DatabaseEvent::ExportCSV, export_csv_handler) .event(DatabaseEvent::ExportCSV, export_csv_handler)
.event(DatabaseEvent::ExportRawDatabaseData, export_raw_database_data_handler)
.event(DatabaseEvent::GetDatabaseSnapshots, get_snapshots_handler) .event(DatabaseEvent::GetDatabaseSnapshots, get_snapshots_handler)
// Field settings // Field settings
.event(DatabaseEvent::GetFieldSettings, get_field_settings_handler) .event(DatabaseEvent::GetFieldSettings, get_field_settings_handler)
@ -385,4 +386,7 @@ pub enum DatabaseEvent {
#[event(input = "DatabaseViewIdPB", output = "RepeatedRowMetaPB")] #[event(input = "DatabaseViewIdPB", output = "RepeatedRowMetaPB")]
GetAllRows = 177, GetAllRows = 177,
#[event(input = "DatabaseViewIdPB", output = "DatabaseExportDataPB")]
ExportRawDatabaseData = 178,
} }

View File

@ -345,7 +345,7 @@ impl DatabaseManager {
Ok(()) Ok(())
} }
pub async fn duplicate_database(&self, view_id: &str) -> FlowyResult<Vec<u8>> { pub async fn get_database_json_bytes(&self, view_id: &str) -> FlowyResult<Vec<u8>> {
let lock = self.workspace_database()?; let lock = self.workspace_database()?;
let wdb = lock.read().await; let wdb = lock.read().await;
let data = wdb.get_database_data(view_id).await?; let data = wdb.get_database_data(view_id).await?;
@ -353,6 +353,14 @@ impl DatabaseManager {
Ok(json_bytes) Ok(json_bytes)
} }
pub async fn get_database_json_string(&self, view_id: &str) -> FlowyResult<String> {
let lock = self.workspace_database()?;
let wdb = lock.read().await;
let data = wdb.get_database_data(view_id).await?;
let json_string = serde_json::to_string(&data)?;
Ok(json_string)
}
/// Create a new database with the given data that can be deserialized to [DatabaseData]. /// Create a new database with the given data that can be deserialized to [DatabaseData].
#[tracing::instrument(level = "trace", skip_all, err)] #[tracing::instrument(level = "trace", skip_all, err)]
pub async fn create_database_with_database_data( pub async fn create_database_with_database_data(