diff --git a/frontend/appflowy_flutter/assets/translations/en.json b/frontend/appflowy_flutter/assets/translations/en.json index ff1d5139c6..db9dd6f54e 100644 --- a/frontend/appflowy_flutter/assets/translations/en.json +++ b/frontend/appflowy_flutter/assets/translations/en.json @@ -476,5 +476,10 @@ "name": "Calendar layout" }, "referencedCalendarPrefix": "View of" + }, + "errorDialog": { + "title": "AppFlowy Error", + "howToFixFallback": "We're sorry for the inconvenience! Submit an issue on our GitHub page that describes your error.", + "github": "View on GitHub" } -} \ No newline at end of file +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/board/presentation/board_page.dart b/frontend/appflowy_flutter/lib/plugins/database_view/board/presentation/board_page.dart index c25d791c46..c09b798e31 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/board/presentation/board_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/board/presentation/board_page.dart @@ -95,7 +95,7 @@ class BoardPage extends StatelessWidget { (_) => BoardContent( onEditStateChanged: onEditStateChanged, ), - (err) => FlowyErrorPage(err.toString()), + (err) => FlowyErrorPage.message(err.toString(), howToFix: LocaleKeys.errorDialog_howToFixFallback.tr(),), ); }, ); diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/grid_page.dart b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/grid_page.dart index 836543d7ac..b8f0bc70dc 100755 --- a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/grid_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/grid_page.dart @@ -125,7 +125,10 @@ class _GridPageState extends State { (_) => GridShortcuts( child: GridPageContent(view: widget.view), ), - (err) => FlowyErrorPage(err.toString()), + (err) => FlowyErrorPage.message( + err.toString(), + howToFix: LocaleKeys.errorDialog_howToFixFallback.tr(), + ), ), ); }, diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/row_document.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/row_document.dart index 1d2b2771be..20ad2bc402 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/row_document.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/row_document.dart @@ -1,9 +1,11 @@ +import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/database_view/grid/application/row/row_document_bloc.dart'; import 'package:appflowy/plugins/document/application/doc_bloc.dart'; import 'package:appflowy/plugins/document/presentation/editor_page.dart'; import 'package:appflowy/plugins/document/presentation/editor_style.dart'; import 'package:appflowy/plugins/document/presentation/more/cubit/document_appearance_cubit.dart'; import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.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_bloc/flutter_bloc.dart'; @@ -35,8 +37,9 @@ class RowDocument extends StatelessWidget { loading: () => const Center( child: CircularProgressIndicator.adaptive(), ), - error: (error) => FlowyErrorPage( + error: (error) => FlowyErrorPage.message( error.toString(), + howToFix: LocaleKeys.errorDialog_howToFixFallback.tr(), ), finish: () => RowEditor( viewPB: state.viewPB!, @@ -94,8 +97,9 @@ class _RowEditorState extends State { ), finish: (result) { return result.fold( - (error) => FlowyErrorPage( + (error) => FlowyErrorPage.message( error.toString(), + howToFix: LocaleKeys.errorDialog_howToFixFallback.tr(), ), (_) { final editorState = documentBloc.editorState; diff --git a/frontend/appflowy_flutter/lib/plugins/document/document_page.dart b/frontend/appflowy_flutter/lib/plugins/document/document_page.dart index ecebf0bf8b..2e448c99bc 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/document_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/document_page.dart @@ -1,6 +1,7 @@ import 'dart:convert'; import 'dart:io'; +import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/document/application/doc_bloc.dart'; import 'package:appflowy/plugins/document/presentation/banner.dart'; import 'package:appflowy/plugins/document/presentation/editor_page.dart'; @@ -14,11 +15,11 @@ import 'package:appflowy_backend/protobuf/flowy-document2/protobuf.dart' hide DocumentEvent; import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.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/widget/error_page.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:intl/intl.dart'; import 'package:path/path.dart' as p; class DocumentPage extends StatefulWidget { @@ -65,7 +66,10 @@ class _DocumentPageState extends State { return state.loadingState.when( loading: () => const SizedBox.shrink(), finish: (result) => result.fold( - (error) => FlowyErrorPage(error.toString()), + (error) => FlowyErrorPage.message( + error.toString(), + howToFix: LocaleKeys.errorDialog_howToFixFallback.tr(), + ), (data) { if (state.forceClose) { widget.onDeleted(); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/insert_page_command.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/insert_page_command.dart index 93ac919f97..debf00f4e1 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/insert_page_command.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/insert_page_command.dart @@ -2,6 +2,7 @@ import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/database_view/application/database_view_service.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; import 'package:appflowy/workspace/application/view/view_service.dart'; +import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:easy_localization/easy_localization.dart'; @@ -31,14 +32,22 @@ extension InsertDatabase on EditorState { await apply(transaction); } - Future insertReferencePage(ViewPB childView) async { + Future insertReferencePage( + ViewPB childView, + ) async { final selection = this.selection; if (selection == null || !selection.isCollapsed) { - return; + throw FlowyError( + msg: + "Could not insert the reference page because the current selection was null or collapsed.", + ); } final node = getNodeAtPath(selection.end.path); if (node == null) { - return; + throw FlowyError( + msg: + "Could not insert the reference page because the current node at the selection does not exist.", + ); } // get the database id that the view is associated with @@ -60,10 +69,11 @@ extension InsertDatabase on EditorState { databaseId: databaseId, ).then((value) => value.swap().toOption().toNullable()); - // TODO(a-wallen): Show error dialog here. - // Maybe extend the FlowyErrorPage. if (ref == null) { - return; + throw FlowyError( + msg: + "The `ViewBackendService` failed to create a database reference view", + ); } final transaction = this.transaction; diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/link_to_page_widget.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/link_to_page_widget.dart index 715ebb6f20..902a551f65 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/link_to_page_widget.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/link_to_page_widget.dart @@ -1,11 +1,14 @@ import 'package:appflowy/plugins/document/presentation/editor_plugins/base/insert_page_command.dart'; import 'package:appflowy/workspace/application/view/view_ext.dart'; import 'package:appflowy/workspace/application/view/view_service.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-folder2/view.pb.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:flowy_infra/image.dart'; import 'package:flowy_infra_ui/style_widget/button.dart'; import 'package:flowy_infra_ui/style_widget/text.dart'; +import 'package:flowy_infra_ui/widget/error_page.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; @@ -37,9 +40,19 @@ void showLinkToPageMenu( editorState: editorState, layoutType: pageType, hintText: pageType.toHintText(), - onSelected: (appPB, viewPB) { - editorState.insertReferencePage(viewPB); - linkToPageMenuEntry.remove(); + onSelected: (appPB, viewPB) async { + try { + await editorState.insertReferencePage(viewPB); + linkToPageMenuEntry.remove(); + } on FlowyError catch (e) { + Dialogs.show( + FlowyErrorPage.message( + e.msg, + howToFix: LocaleKeys.errorDialog_howToFixFallback.tr(), + ), + context, + ); + } }, ), ), diff --git a/frontend/appflowy_flutter/lib/user/presentation/welcome_screen.dart b/frontend/appflowy_flutter/lib/user/presentation/welcome_screen.dart index 0fb3e2dfea..b8b1361fad 100644 --- a/frontend/appflowy_flutter/lib/user/presentation/welcome_screen.dart +++ b/frontend/appflowy_flutter/lib/user/presentation/welcome_screen.dart @@ -44,7 +44,7 @@ class WelcomeScreen extends StatelessWidget { Widget _renderBody(WelcomeState state) { final body = state.successOrFailure.fold( (_) => _renderList(state.workspaces), - (error) => FlowyErrorPage(error.toString()), + (error) => FlowyErrorPage.message(error.toString(), howToFix: LocaleKeys.errorDialog_howToFixFallback.tr(),), ); return body; } diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/widget/error_page.dart b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/widget/error_page.dart index 0b586ce839..a62daf3632 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/widget/error_page.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/widget/error_page.dart @@ -1,11 +1,191 @@ +import 'package:flowy_infra/image.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:url_launcher/url_launcher.dart'; class FlowyErrorPage extends StatelessWidget { - final String error; - const FlowyErrorPage(this.error, {Key? key}) : super(key: key); + factory FlowyErrorPage.error( + Error e, { + required String howToFix, + Key? key, + }) => + FlowyErrorPage._( + e.toString(), + stackTrace: e.stackTrace?.toString(), + howToFix: howToFix, + key: key, + ); + + factory FlowyErrorPage.message( + String message, { + required String howToFix, + String? stackTrace, + Key? key, + }) => + FlowyErrorPage._( + message, + key: key, + stackTrace: stackTrace, + howToFix: howToFix, + ); + + factory FlowyErrorPage.exception( + Exception e, { + required String howToFix, + String? stackTrace, + Key? key, + }) => + FlowyErrorPage._( + e.toString(), + stackTrace: stackTrace, + key: key, + howToFix: howToFix, + ); + + const FlowyErrorPage._( + this.message, { + required this.howToFix, + this.stackTrace, + super.key, + }); + + static const _titleFontSize = 24.0; + static const _titleToMessagePadding = 8.0; + + final String message; + final String? stackTrace; + final String howToFix; @override Widget build(BuildContext context) { - return Text(error); + return Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + children: [ + const FlowyText.medium( + "AppFlowy Error", + fontSize: _titleFontSize, + ), + const SizedBox( + height: _titleToMessagePadding, + ), + FlowyText.semibold( + message, + ), + const SizedBox( + height: _titleToMessagePadding, + ), + FlowyText.regular( + howToFix, + ), + const SizedBox( + height: _titleToMessagePadding, + ), + const GitHubRedirectButton(), + const SizedBox( + height: _titleToMessagePadding, + ), + if (stackTrace != null) StackTracePreview(stackTrace!), + ], + ), + ); + } +} + +class StackTracePreview extends StatelessWidget { + const StackTracePreview( + this.stackTrace, { + super.key, + }); + + final String stackTrace; + + @override + Widget build(BuildContext context) { + return ConstrainedBox( + constraints: const BoxConstraints( + minWidth: 350, + maxWidth: 450, + ), + child: Card( + elevation: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + clipBehavior: Clip.antiAlias, + margin: EdgeInsets.zero, + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + children: [ + const Align( + alignment: Alignment.centerLeft, + child: FlowyText.semibold( + "Stack Trace", + ), + ), + Container( + height: 120, + padding: const EdgeInsets.symmetric(vertical: 8), + child: SingleChildScrollView( + child: Text( + stackTrace, + style: Theme.of(context).textTheme.bodySmall, + ), + ), + ), + Align( + alignment: Alignment.centerRight, + child: FlowyButton( + hoverColor: Theme.of(context).colorScheme.onBackground, + text: const FlowyText( + "Copy", + ), + useIntrinsicWidth: true, + onTap: () => Clipboard.setData( + ClipboardData(text: stackTrace), + ), + ), + ), + ], + ), + ), + ), + ); + } +} + +class GitHubRedirectButton extends StatelessWidget { + const GitHubRedirectButton({super.key}); + + static const _height = 32.0; + + Uri get _gitHubNewBugUri => Uri( + scheme: 'https', + host: 'github.com', + path: '/AppFlowy-IO/AppFlowy/issues/new', + query: + 'assignees=&labels=&projects=&template=bug_report.yaml&title=%5BBug%5D+', + ); + + @override + Widget build(BuildContext context) { + return FlowyButton( + leftIconSize: const Size.square(_height), + text: const FlowyText( + "AppFlowy", + ), + useIntrinsicWidth: true, + leftIcon: Padding( + padding: const EdgeInsets.all(4.0), + child: svgWidget('login/github-mark'), + ), + onTap: () async { + if (await canLaunchUrl(_gitHubNewBugUri)) { + await launchUrl(_gitHubNewBugUri); + } + }, + ); } } diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/pubspec.yaml b/frontend/appflowy_flutter/packages/flowy_infra_ui/pubspec.yaml index 9cc2a26929..69afe449df 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/pubspec.yaml +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/pubspec.yaml @@ -20,6 +20,7 @@ dependencies: animations: ^2.0.7 loading_indicator: ^3.1.0 async: + url_launcher: ^6.1.11 # Federated Platform Interface flowy_infra_ui_platform_interface: