From 44d8def3ce0b34c20c508ee9575604213f5f02af Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Tue, 16 Jul 2024 13:23:07 +0800 Subject: [PATCH] fix: reset space relationship when clearing cache (#5737) * fix: space's child views didn't update when moving a page into it * chore: remove check logic * feat: integrate fix space into clear cache * fix: translation missing value * chore: update logger version --- frontend/.vscode/launch.json | 2 +- .../database/application/tab_bar_bloc.dart | 2 +- .../lib/shared/feature_flags.dart | 16 +++- .../application/sidebar/space/space_bloc.dart | 18 +++- .../workspace/application/view/view_bloc.dart | 2 +- .../application/view/view_service.dart | 8 +- .../menu/sidebar/space/shared_widget.dart | 15 ++- .../settings/pages/fix_data_widget.dart | 96 ++++++++++++++++++- .../pages/settings_manage_data_view.dart | 22 +++-- .../presentation/widgets/dialogs.dart | 29 ++++++ .../lib/appflowy_backend.dart | 18 ++-- .../packages/appflowy_backend/lib/log.dart | 17 ++-- .../packages/appflowy_backend/pubspec.yaml | 2 +- frontend/appflowy_flutter/pubspec.lock | 6 +- frontend/appflowy_flutter/pubspec.yaml | 1 + frontend/resources/translations/en.json | 10 +- .../flowy-folder/src/manager_observer.rs | 1 - 17 files changed, 215 insertions(+), 50 deletions(-) diff --git a/frontend/.vscode/launch.json b/frontend/.vscode/launch.json index 72d398e0fa..317392654b 100644 --- a/frontend/.vscode/launch.json +++ b/frontend/.vscode/launch.json @@ -138,4 +138,4 @@ "cwd": "${workspaceRoot}/appflowy_tauri/" }, ] -} \ No newline at end of file +} diff --git a/frontend/appflowy_flutter/lib/plugins/database/application/tab_bar_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database/application/tab_bar_bloc.dart index 943f8ec20c..e4eedd87fd 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/application/tab_bar_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/application/tab_bar_bloc.dart @@ -52,7 +52,7 @@ class DatabaseTabBarBloc _createLinkedView(layout.layoutType, name ?? layout.layoutName); }, deleteView: (String viewId) async { - final result = await ViewBackendService.delete(viewId: viewId); + final result = await ViewBackendService.deleteView(viewId: viewId); result.fold( (l) {}, (r) => Log.error(r), diff --git a/frontend/appflowy_flutter/lib/shared/feature_flags.dart b/frontend/appflowy_flutter/lib/shared/feature_flags.dart index 31e61ebb08..2c727a8d46 100644 --- a/frontend/appflowy_flutter/lib/shared/feature_flags.dart +++ b/frontend/appflowy_flutter/lib/shared/feature_flags.dart @@ -35,6 +35,9 @@ enum FeatureFlag { // used for controlling whether to show plan+billing options in settings planBilling, + // used for space design + spaceDesign, + // used for ignore the conflicted feature flag unknown; @@ -88,6 +91,8 @@ enum FeatureFlag { bool get isOn { if ([ + // release this feature in version 0.6.1 + FeatureFlag.spaceDesign, // release this feature in version 0.5.9 FeatureFlag.search, // release this feature in version 0.5.6 @@ -105,15 +110,16 @@ enum FeatureFlag { } switch (this) { + case FeatureFlag.search: + case FeatureFlag.syncDocument: + case FeatureFlag.syncDatabase: + case FeatureFlag.spaceDesign: + return true; case FeatureFlag.collaborativeWorkspace: case FeatureFlag.membersSettings: case FeatureFlag.planBilling: case FeatureFlag.unknown: return false; - case FeatureFlag.search: - case FeatureFlag.syncDocument: - case FeatureFlag.syncDatabase: - return true; } } @@ -131,6 +137,8 @@ enum FeatureFlag { return 'if it\'s on, the command palette and search button will be available'; case FeatureFlag.planBilling: return 'if it\'s on, plan and billing pages will be available in Settings'; + case FeatureFlag.spaceDesign: + return 'if it\'s on, the space design feature will be available'; case FeatureFlag.unknown: return ''; } diff --git a/frontend/appflowy_flutter/lib/workspace/application/sidebar/space/space_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/sidebar/space/space_bloc.dart index f152945d60..9d79ca8550 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/sidebar/space/space_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/sidebar/space/space_bloc.dart @@ -141,7 +141,7 @@ class SpaceBloc extends Bloc { if (deletedSpace == null) { return; } - await ViewBackendService.delete(viewId: deletedSpace.id); + await ViewBackendService.deleteView(viewId: deletedSpace.id); }, rename: (space, name) async { add(SpaceEvent.update(name: name)); @@ -433,6 +433,7 @@ class SpaceBloc extends Bloc { workspaceId: workspaceId, )..start( sectionChanged: (result) async { + Log.info('did receive section views changed'); add(const SpaceEvent.didReceiveSpaceUpdate()); }, ); @@ -503,6 +504,7 @@ class SpaceBloc extends Bloc { if (_workspaceId == null) { return false; } + try { final user = await UserBackendService.getCurrentUserProfile().getOrThrow(); @@ -526,6 +528,13 @@ class SpaceBloc extends Bloc { (e) => e.isSpace && e.spacePermission == SpacePermission.publicToAll, ); publicViews = publicViews.where((e) => !e.isSpace).toList(); + + for (final view in publicViews) { + Log.info( + 'migrating: the public view should be migrated: ${view.name}(${view.id})', + ); + } + // if there is already a public space, don't migrate the public space // only migrate the public space if there are any public views if (publicViews.isEmpty || containsPublicSpace) { @@ -568,6 +577,13 @@ class SpaceBloc extends Bloc { (e) => e.isSpace && e.spacePermission == SpacePermission.private, ); privateViews = privateViews.where((e) => !e.isSpace).toList(); + + for (final view in privateViews) { + Log.info( + 'migrating: the private view should be migrated: ${view.name}(${view.id})', + ); + } + if (privateViews.isEmpty || containsPrivateSpace) { return true; } diff --git a/frontend/appflowy_flutter/lib/workspace/application/view/view_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/view/view_bloc.dart index 44300468a1..2a1ce58400 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/view/view_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/view/view_bloc.dart @@ -143,7 +143,7 @@ class ViewBloc extends Bloc { // unpublish the page and all its child pages if they are published await _unpublishPage(view); - final result = await ViewBackendService.delete(viewId: view.id); + final result = await ViewBackendService.deleteView(viewId: view.id); emit( result.fold( 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 f07b90ef4f..d1c8659370 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/view/view_service.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/view/view_service.dart @@ -122,17 +122,17 @@ class ViewBackendService { }); } - static Future> delete({ + static Future> deleteView({ required String viewId, }) { final request = RepeatedViewIdPB.create()..items.add(viewId); return FolderEventDeleteView(request).send(); } - static Future> deleteView({ - required String viewId, + static Future> deleteViews({ + required List viewIds, }) { - final request = RepeatedViewIdPB.create()..items.add(viewId); + final request = RepeatedViewIdPB.create()..items.addAll(viewIds); return FolderEventDeleteView(request).send(); } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/space/shared_widget.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/space/shared_widget.dart index efd98647af..f068666958 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/space/shared_widget.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/space/shared_widget.dart @@ -292,11 +292,15 @@ class ConfirmPopup extends StatefulWidget { required this.title, required this.description, required this.onConfirm, + this.confirmLabel, + this.confirmButtonColor, }); final String title; final String description; final VoidCallback onConfirm; + final String? confirmLabel; + final Color? confirmButtonColor; final ConfirmPopupStyle style; @override @@ -376,8 +380,9 @@ class _ConfirmPopupState extends State { widget.onConfirm(); Navigator.of(context).pop(); }, - confirmButtonName: LocaleKeys.button_ok.tr(), - confirmButtonColor: Theme.of(context).colorScheme.primary, + confirmButtonName: widget.confirmLabel ?? LocaleKeys.button_ok.tr(), + confirmButtonColor: widget.confirmButtonColor ?? + Theme.of(context).colorScheme.primary, ); case ConfirmPopupStyle.cancelAndOk: return SpaceCancelOrConfirmButton( @@ -386,8 +391,10 @@ class _ConfirmPopupState extends State { widget.onConfirm(); Navigator.of(context).pop(); }, - confirmButtonName: LocaleKeys.space_delete.tr(), - confirmButtonColor: Theme.of(context).colorScheme.error, + confirmButtonName: + widget.confirmLabel ?? LocaleKeys.space_delete.tr(), + confirmButtonColor: + widget.confirmButtonColor ?? Theme.of(context).colorScheme.error, ); } } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/fix_data_widget.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/fix_data_widget.dart index 6bc7220abf..6f556ec5b6 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/fix_data_widget.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/fix_data_widget.dart @@ -7,6 +7,7 @@ import 'package:appflowy/workspace/presentation/settings/shared/single_setting_a import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:appflowy_result/appflowy_result.dart'; +import 'package:collection/collection.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; @@ -24,7 +25,7 @@ class FixDataWidget extends StatelessWidget { .tr(), buttonLabel: LocaleKeys.settings_manageDataPage_data_fixButton.tr(), onPressed: () { - FixDataManager.checkWorkspaceHealth(dryRun: true); + WorkspaceDataManager.checkWorkspaceHealth(dryRun: true); }, ), ], @@ -32,7 +33,7 @@ class FixDataWidget extends StatelessWidget { } } -class FixDataManager { +class WorkspaceDataManager { static Future checkWorkspaceHealth({ required bool dryRun, }) async { @@ -63,6 +64,9 @@ class FixDataManager { // check the health of the spaces await checkSpaceHealth(workspace: workspace, allViews: allViews); + // check the health of the views + await checkViewHealth(workspace: workspace, allViews: allViews); + // add other checks here // ... } catch (e) { @@ -83,7 +87,6 @@ class FixDataManager { workspaceChildViews.map((e) => e.id).toSet(); final spaces = allViews.where((e) => e.isSpace).toList(); - // for (final space in spaces) { // the space is the top level view, so its parent view id should be the workspace id // and the workspace should have the space in its child views @@ -106,6 +109,93 @@ class FixDataManager { } } + static Future> checkViewHealth({ + ViewPB? workspace, + List? allViews, + bool dryRun = true, + }) async { + // Views whose parent view does not have the view in its child views + final List unlistedChildViews = []; + // Views whose parent is not in allViews + final List orphanViews = []; + + try { + if (workspace == null || allViews == null) { + final currentWorkspace = + await UserBackendService.getCurrentWorkspace().getOrThrow(); + // get all the views in the workspace + final result = await ViewBackendService.getAllViews().getOrThrow(); + allViews = result.items; + workspace = allViews.firstWhereOrNull( + (e) => e.id == currentWorkspace.id, + ); + } + + for (final view in allViews) { + if (view.parentViewId == '') { + continue; + } + + final parentView = allViews.firstWhereOrNull( + (e) => e.id == view.parentViewId, + ); + + if (parentView == null) { + orphanViews.add(view); + continue; + } + + final childViewsOfParent = + await ViewBackendService.getChildViews(viewId: parentView.id) + .getOrThrow(); + final result = childViewsOfParent.any((e) => e.id == view.id); + if (!result) { + unlistedChildViews.add(view); + } + } + } catch (e) { + Log.error('Failed to check space health: $e'); + return []; + } + + for (final view in unlistedChildViews) { + Log.info( + '[workspace] found an issue: view is not in the parent view\'s child views, view: ${view.toProto3Json()}}', + ); + } + + for (final view in orphanViews) { + Log.debug('[workspace] orphanViews: ${view.toProto3Json()}'); + } + + if (!dryRun && unlistedChildViews.isNotEmpty) { + Log.info( + '[workspace] start to fix ${unlistedChildViews.length} unlistedChildViews ...', + ); + for (final view in unlistedChildViews) { + // move the view to the parent view if it is not in the parent view's child views + Log.info( + '[workspace] move view: $view to its parent view ${view.parentViewId}', + ); + await ViewBackendService.moveViewV2( + viewId: view.id, + newParentId: view.parentViewId, + prevViewId: null, + ); + } + + Log.info('[workspace] end to fix unlistedChildViews'); + } + + if (unlistedChildViews.isEmpty && orphanViews.isEmpty) { + Log.info('[workspace] all views are healthy'); + } + + Log.info('[workspace] done checking view health'); + + return unlistedChildViews; + } + static void dumpViews(String prefix, List views) { for (int i = 0; i < views.length; i++) { final view = views[i]; diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_manage_data_view.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_manage_data_view.dart index 5b8965ffbe..3e169eb806 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_manage_data_view.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_manage_data_view.dart @@ -126,26 +126,34 @@ class SettingsManageDataView extends StatelessWidget { buttonLabel: LocaleKeys.settings_manageDataPage_cache_title.tr(), onPressed: () { - SettingsAlertDialog( + showCancelAndConfirmDialog( + context: context, title: LocaleKeys .settings_manageDataPage_cache_dialog_title .tr(), - subtitle: LocaleKeys + description: LocaleKeys .settings_manageDataPage_cache_dialog_description .tr(), - confirm: () async { + confirmLabel: LocaleKeys.button_ok.tr(), + onConfirm: () async { + // clear all cache await getIt().clearAllCache(); + + // check the workspace and space health + await WorkspaceDataManager.checkViewHealth( + dryRun: false, + ); + if (context.mounted) { - showSnackBarMessage( + showToastNotification( context, - LocaleKeys + message: LocaleKeys .settings_manageDataPage_cache_dialog_successHint .tr(), ); - Navigator.of(context).pop(); } }, - ).show(context); + ); }, ), ], diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/dialogs.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/dialogs.dart index 32949e54f8..d453e5b057 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/dialogs.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/dialogs.dart @@ -369,3 +369,32 @@ Future showConfirmDialog({ }, ); } + +Future showCancelAndConfirmDialog({ + required BuildContext context, + required String title, + required String description, + VoidCallback? onConfirm, + String? confirmLabel, +}) { + return showDialog( + context: context, + builder: (_) { + return Dialog( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12.0), + ), + child: SizedBox( + width: 440, + child: ConfirmPopup( + title: title, + description: description, + onConfirm: () => onConfirm?.call(), + confirmLabel: confirmLabel, + confirmButtonColor: Theme.of(context).colorScheme.primary, + ), + ), + ); + }, + ); +} diff --git a/frontend/appflowy_flutter/packages/appflowy_backend/lib/appflowy_backend.dart b/frontend/appflowy_flutter/packages/appflowy_backend/lib/appflowy_backend.dart index 9c1a1de27c..69e287f117 100644 --- a/frontend/appflowy_flutter/packages/appflowy_backend/lib/appflowy_backend.dart +++ b/frontend/appflowy_flutter/packages/appflowy_backend/lib/appflowy_backend.dart @@ -1,16 +1,19 @@ -export 'package:async/async.dart'; import 'dart:async'; import 'dart:convert'; +import 'dart:ffi'; +import 'dart:io'; +import 'dart:isolate'; + import 'package:appflowy_backend/rust_stream.dart'; +import 'package:ffi/ffi.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; -import 'dart:ffi'; -import 'ffi.dart' as ffi; -import 'package:ffi/ffi.dart'; -import 'dart:isolate'; -import 'dart:io'; import 'package:logger/logger.dart'; +import 'ffi.dart' as ffi; + +export 'package:async/async.dart'; + enum ExceptionType { AppearanceSettingsIsEmpty, } @@ -72,7 +75,8 @@ class RustLogStreamReceiver { lineLength: 120, // width of the output colors: false, // Colorful log messages printEmojis: false, // Print an emoji for each log message - printTime: false, // Should each log print contain a timestamp + dateTimeFormat: + DateTimeFormat.none, // Should each log print contain a timestamp ), level: kDebugMode ? Level.trace : Level.info, ); diff --git a/frontend/appflowy_flutter/packages/appflowy_backend/lib/log.dart b/frontend/appflowy_flutter/packages/appflowy_backend/lib/log.dart index dc79e655d1..4e52ba2c82 100644 --- a/frontend/appflowy_flutter/packages/appflowy_backend/lib/log.dart +++ b/frontend/appflowy_flutter/packages/appflowy_backend/lib/log.dart @@ -1,5 +1,6 @@ // ignore: import_of_legacy_library_into_null_safe import 'dart:ffi'; + import 'package:ffi/ffi.dart' as ffi; import 'package:flutter/foundation.dart'; import 'package:logger/logger.dart'; @@ -14,13 +15,15 @@ class Log { Log() { _logger = Logger( printer: PrettyPrinter( - methodCount: 2, // number of method calls to be displayed - errorMethodCount: 8, // number of method calls if stacktrace is provided - lineLength: 120, // width of the output - colors: true, // Colorful log messages - printEmojis: true, // Print an emoji for each log message - printTime: false, // Should each log print contain a timestamp - ), + methodCount: 2, // number of method calls to be displayed + errorMethodCount: + 8, // number of method calls if stacktrace is provided + lineLength: 120, // width of the output + colors: true, // Colorful log messages + printEmojis: true, // Print an emoji for each log message + dateTimeFormat: + DateTimeFormat.none // Should each log print contain a timestamp + ), level: kDebugMode ? Level.trace : Level.info, ); } diff --git a/frontend/appflowy_flutter/packages/appflowy_backend/pubspec.yaml b/frontend/appflowy_flutter/packages/appflowy_backend/pubspec.yaml index c014da75df..51ad23fde4 100644 --- a/frontend/appflowy_flutter/packages/appflowy_backend/pubspec.yaml +++ b/frontend/appflowy_flutter/packages/appflowy_backend/pubspec.yaml @@ -15,7 +15,7 @@ dependencies: isolates: ^3.0.3+8 protobuf: ^3.1.0 freezed_annotation: - logger: ^2.0.0 + logger: ^2.4.0 plugin_platform_interface: ^2.1.3 json_annotation: ^4.7.0 appflowy_result: diff --git a/frontend/appflowy_flutter/pubspec.lock b/frontend/appflowy_flutter/pubspec.lock index f330999d38..3d524efc45 100644 --- a/frontend/appflowy_flutter/pubspec.lock +++ b/frontend/appflowy_flutter/pubspec.lock @@ -1251,13 +1251,13 @@ packages: source: hosted version: "0.1.5" logger: - dependency: transitive + dependency: "direct main" description: name: logger - sha256: "6bbb9d6f7056729537a4309bda2e74e18e5d9f14302489cc1e93f33b3fe32cac" + sha256: "697d067c60c20999686a0add96cf6aba723b3aa1f83ecf806a8097231529ec32" url: "https://pub.dev" source: hosted - version: "2.0.2+1" + version: "2.4.0" logging: dependency: transitive description: diff --git a/frontend/appflowy_flutter/pubspec.yaml b/frontend/appflowy_flutter/pubspec.yaml index b580961362..2e339373ad 100644 --- a/frontend/appflowy_flutter/pubspec.yaml +++ b/frontend/appflowy_flutter/pubspec.yaml @@ -144,6 +144,7 @@ dependencies: markdown_widget: ^2.3.2+6 desktop_drop: ^0.4.4 markdown: + logger: ^2.4.0 # Window Manager for MacOS and Linux window_manager: ^0.3.9 diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index 9523d56620..3080596c3a 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -494,10 +494,10 @@ }, "cache": { "title": "Clear cache", - "description": "Clear app cache, this can help resolve issues like images or fonts not loading. This will not affect your data.", + "description": "Help resolve issues like image not loading, missing pages in a space, and fonts not loading. This will not affect your data.", "dialog": { - "title": "Are you sure?", - "description": "Clearing the cache will cause images and fonts to be re-downloaded on load. This action will not remove or modify your data.", + "title": "Clear cache", + "description": "Help resolve issues like image not loading, missing pages in a space, and fonts not loading. This will not affect your data.", "successHint": "Cache cleared!" } }, @@ -757,7 +757,7 @@ "freeLabels": { "itemOne": "charged per workspace", "itemTwo": "3", - "itemThree": "", + "itemThree": " ", "itemFour": "0", "itemFive": "5 GB", "itemSix": "yes", @@ -767,7 +767,7 @@ "proLabels": { "itemOne": "charged per workspace", "itemTwo": "up to 10", - "itemThree": "", + "itemThree": " ", "itemFour": "10 guests billed as one seat", "itemFive": "unlimited", "itemSix": "yes", diff --git a/frontend/rust-lib/flowy-folder/src/manager_observer.rs b/frontend/rust-lib/flowy-folder/src/manager_observer.rs index ef604b3a11..91bd450a70 100644 --- a/frontend/rust-lib/flowy-folder/src/manager_observer.rs +++ b/frontend/rust-lib/flowy-folder/src/manager_observer.rs @@ -235,7 +235,6 @@ pub(crate) fn notify_did_update_section_views(workspace_id: &str, folder: &Folde private_views.len() ); - // TODO(Lucas.xu) - Only notify the section changed, not the public/private both. // Notify the public views send_notification(workspace_id, FolderNotification::DidUpdateSectionViews) .payload(SectionViewsPB {