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
This commit is contained in:
Lucas.Xu 2024-07-16 13:23:07 +08:00 committed by GitHub
parent c6ad57f11d
commit 44d8def3ce
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 215 additions and 50 deletions

View File

@ -138,4 +138,4 @@
"cwd": "${workspaceRoot}/appflowy_tauri/" "cwd": "${workspaceRoot}/appflowy_tauri/"
}, },
] ]
} }

View File

@ -52,7 +52,7 @@ class DatabaseTabBarBloc
_createLinkedView(layout.layoutType, name ?? layout.layoutName); _createLinkedView(layout.layoutType, name ?? layout.layoutName);
}, },
deleteView: (String viewId) async { deleteView: (String viewId) async {
final result = await ViewBackendService.delete(viewId: viewId); final result = await ViewBackendService.deleteView(viewId: viewId);
result.fold( result.fold(
(l) {}, (l) {},
(r) => Log.error(r), (r) => Log.error(r),

View File

@ -35,6 +35,9 @@ enum FeatureFlag {
// used for controlling whether to show plan+billing options in settings // used for controlling whether to show plan+billing options in settings
planBilling, planBilling,
// used for space design
spaceDesign,
// used for ignore the conflicted feature flag // used for ignore the conflicted feature flag
unknown; unknown;
@ -88,6 +91,8 @@ enum FeatureFlag {
bool get isOn { bool get isOn {
if ([ if ([
// release this feature in version 0.6.1
FeatureFlag.spaceDesign,
// release this feature in version 0.5.9 // release this feature in version 0.5.9
FeatureFlag.search, FeatureFlag.search,
// release this feature in version 0.5.6 // release this feature in version 0.5.6
@ -105,15 +110,16 @@ enum FeatureFlag {
} }
switch (this) { switch (this) {
case FeatureFlag.search:
case FeatureFlag.syncDocument:
case FeatureFlag.syncDatabase:
case FeatureFlag.spaceDesign:
return true;
case FeatureFlag.collaborativeWorkspace: case FeatureFlag.collaborativeWorkspace:
case FeatureFlag.membersSettings: case FeatureFlag.membersSettings:
case FeatureFlag.planBilling: case FeatureFlag.planBilling:
case FeatureFlag.unknown: case FeatureFlag.unknown:
return false; 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'; return 'if it\'s on, the command palette and search button will be available';
case FeatureFlag.planBilling: case FeatureFlag.planBilling:
return 'if it\'s on, plan and billing pages will be available in Settings'; 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: case FeatureFlag.unknown:
return ''; return '';
} }

View File

@ -141,7 +141,7 @@ class SpaceBloc extends Bloc<SpaceEvent, SpaceState> {
if (deletedSpace == null) { if (deletedSpace == null) {
return; return;
} }
await ViewBackendService.delete(viewId: deletedSpace.id); await ViewBackendService.deleteView(viewId: deletedSpace.id);
}, },
rename: (space, name) async { rename: (space, name) async {
add(SpaceEvent.update(name: name)); add(SpaceEvent.update(name: name));
@ -433,6 +433,7 @@ class SpaceBloc extends Bloc<SpaceEvent, SpaceState> {
workspaceId: workspaceId, workspaceId: workspaceId,
)..start( )..start(
sectionChanged: (result) async { sectionChanged: (result) async {
Log.info('did receive section views changed');
add(const SpaceEvent.didReceiveSpaceUpdate()); add(const SpaceEvent.didReceiveSpaceUpdate());
}, },
); );
@ -503,6 +504,7 @@ class SpaceBloc extends Bloc<SpaceEvent, SpaceState> {
if (_workspaceId == null) { if (_workspaceId == null) {
return false; return false;
} }
try { try {
final user = final user =
await UserBackendService.getCurrentUserProfile().getOrThrow(); await UserBackendService.getCurrentUserProfile().getOrThrow();
@ -526,6 +528,13 @@ class SpaceBloc extends Bloc<SpaceEvent, SpaceState> {
(e) => e.isSpace && e.spacePermission == SpacePermission.publicToAll, (e) => e.isSpace && e.spacePermission == SpacePermission.publicToAll,
); );
publicViews = publicViews.where((e) => !e.isSpace).toList(); 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 // if there is already a public space, don't migrate the public space
// only migrate the public space if there are any public views // only migrate the public space if there are any public views
if (publicViews.isEmpty || containsPublicSpace) { if (publicViews.isEmpty || containsPublicSpace) {
@ -568,6 +577,13 @@ class SpaceBloc extends Bloc<SpaceEvent, SpaceState> {
(e) => e.isSpace && e.spacePermission == SpacePermission.private, (e) => e.isSpace && e.spacePermission == SpacePermission.private,
); );
privateViews = privateViews.where((e) => !e.isSpace).toList(); 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) { if (privateViews.isEmpty || containsPrivateSpace) {
return true; return true;
} }

View File

@ -143,7 +143,7 @@ class ViewBloc extends Bloc<ViewEvent, ViewState> {
// unpublish the page and all its child pages if they are published // unpublish the page and all its child pages if they are published
await _unpublishPage(view); await _unpublishPage(view);
final result = await ViewBackendService.delete(viewId: view.id); final result = await ViewBackendService.deleteView(viewId: view.id);
emit( emit(
result.fold( result.fold(

View File

@ -122,17 +122,17 @@ class ViewBackendService {
}); });
} }
static Future<FlowyResult<void, FlowyError>> delete({ static Future<FlowyResult<void, FlowyError>> deleteView({
required String viewId, required String viewId,
}) { }) {
final request = RepeatedViewIdPB.create()..items.add(viewId); final request = RepeatedViewIdPB.create()..items.add(viewId);
return FolderEventDeleteView(request).send(); return FolderEventDeleteView(request).send();
} }
static Future<FlowyResult<void, FlowyError>> deleteView({ static Future<FlowyResult<void, FlowyError>> deleteViews({
required String viewId, required List<String> viewIds,
}) { }) {
final request = RepeatedViewIdPB.create()..items.add(viewId); final request = RepeatedViewIdPB.create()..items.addAll(viewIds);
return FolderEventDeleteView(request).send(); return FolderEventDeleteView(request).send();
} }

View File

@ -292,11 +292,15 @@ class ConfirmPopup extends StatefulWidget {
required this.title, required this.title,
required this.description, required this.description,
required this.onConfirm, required this.onConfirm,
this.confirmLabel,
this.confirmButtonColor,
}); });
final String title; final String title;
final String description; final String description;
final VoidCallback onConfirm; final VoidCallback onConfirm;
final String? confirmLabel;
final Color? confirmButtonColor;
final ConfirmPopupStyle style; final ConfirmPopupStyle style;
@override @override
@ -376,8 +380,9 @@ class _ConfirmPopupState extends State<ConfirmPopup> {
widget.onConfirm(); widget.onConfirm();
Navigator.of(context).pop(); Navigator.of(context).pop();
}, },
confirmButtonName: LocaleKeys.button_ok.tr(), confirmButtonName: widget.confirmLabel ?? LocaleKeys.button_ok.tr(),
confirmButtonColor: Theme.of(context).colorScheme.primary, confirmButtonColor: widget.confirmButtonColor ??
Theme.of(context).colorScheme.primary,
); );
case ConfirmPopupStyle.cancelAndOk: case ConfirmPopupStyle.cancelAndOk:
return SpaceCancelOrConfirmButton( return SpaceCancelOrConfirmButton(
@ -386,8 +391,10 @@ class _ConfirmPopupState extends State<ConfirmPopup> {
widget.onConfirm(); widget.onConfirm();
Navigator.of(context).pop(); Navigator.of(context).pop();
}, },
confirmButtonName: LocaleKeys.space_delete.tr(), confirmButtonName:
confirmButtonColor: Theme.of(context).colorScheme.error, widget.confirmLabel ?? LocaleKeys.space_delete.tr(),
confirmButtonColor:
widget.confirmButtonColor ?? Theme.of(context).colorScheme.error,
); );
} }
} }

View File

@ -7,6 +7,7 @@ import 'package:appflowy/workspace/presentation/settings/shared/single_setting_a
import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:appflowy_result/appflowy_result.dart'; import 'package:appflowy_result/appflowy_result.dart';
import 'package:collection/collection.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -24,7 +25,7 @@ class FixDataWidget extends StatelessWidget {
.tr(), .tr(),
buttonLabel: LocaleKeys.settings_manageDataPage_data_fixButton.tr(), buttonLabel: LocaleKeys.settings_manageDataPage_data_fixButton.tr(),
onPressed: () { onPressed: () {
FixDataManager.checkWorkspaceHealth(dryRun: true); WorkspaceDataManager.checkWorkspaceHealth(dryRun: true);
}, },
), ),
], ],
@ -32,7 +33,7 @@ class FixDataWidget extends StatelessWidget {
} }
} }
class FixDataManager { class WorkspaceDataManager {
static Future<void> checkWorkspaceHealth({ static Future<void> checkWorkspaceHealth({
required bool dryRun, required bool dryRun,
}) async { }) async {
@ -63,6 +64,9 @@ class FixDataManager {
// check the health of the spaces // check the health of the spaces
await checkSpaceHealth(workspace: workspace, allViews: allViews); await checkSpaceHealth(workspace: workspace, allViews: allViews);
// check the health of the views
await checkViewHealth(workspace: workspace, allViews: allViews);
// add other checks here // add other checks here
// ... // ...
} catch (e) { } catch (e) {
@ -83,7 +87,6 @@ class FixDataManager {
workspaceChildViews.map((e) => e.id).toSet(); workspaceChildViews.map((e) => e.id).toSet();
final spaces = allViews.where((e) => e.isSpace).toList(); final spaces = allViews.where((e) => e.isSpace).toList();
//
for (final space in spaces) { for (final space in spaces) {
// the space is the top level view, so its parent view id should be the workspace id // 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 // and the workspace should have the space in its child views
@ -106,6 +109,93 @@ class FixDataManager {
} }
} }
static Future<List<ViewPB>> checkViewHealth({
ViewPB? workspace,
List<ViewPB>? allViews,
bool dryRun = true,
}) async {
// Views whose parent view does not have the view in its child views
final List<ViewPB> unlistedChildViews = [];
// Views whose parent is not in allViews
final List<ViewPB> 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<ViewPB> views) { static void dumpViews(String prefix, List<ViewPB> views) {
for (int i = 0; i < views.length; i++) { for (int i = 0; i < views.length; i++) {
final view = views[i]; final view = views[i];

View File

@ -126,26 +126,34 @@ class SettingsManageDataView extends StatelessWidget {
buttonLabel: buttonLabel:
LocaleKeys.settings_manageDataPage_cache_title.tr(), LocaleKeys.settings_manageDataPage_cache_title.tr(),
onPressed: () { onPressed: () {
SettingsAlertDialog( showCancelAndConfirmDialog(
context: context,
title: LocaleKeys title: LocaleKeys
.settings_manageDataPage_cache_dialog_title .settings_manageDataPage_cache_dialog_title
.tr(), .tr(),
subtitle: LocaleKeys description: LocaleKeys
.settings_manageDataPage_cache_dialog_description .settings_manageDataPage_cache_dialog_description
.tr(), .tr(),
confirm: () async { confirmLabel: LocaleKeys.button_ok.tr(),
onConfirm: () async {
// clear all cache
await getIt<FlowyCacheManager>().clearAllCache(); await getIt<FlowyCacheManager>().clearAllCache();
// check the workspace and space health
await WorkspaceDataManager.checkViewHealth(
dryRun: false,
);
if (context.mounted) { if (context.mounted) {
showSnackBarMessage( showToastNotification(
context, context,
LocaleKeys message: LocaleKeys
.settings_manageDataPage_cache_dialog_successHint .settings_manageDataPage_cache_dialog_successHint
.tr(), .tr(),
); );
Navigator.of(context).pop();
} }
}, },
).show(context); );
}, },
), ),
], ],

View File

@ -369,3 +369,32 @@ Future<void> showConfirmDialog({
}, },
); );
} }
Future<void> 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,
),
),
);
},
);
}

View File

@ -1,16 +1,19 @@
export 'package:async/async.dart';
import 'dart:async'; import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'dart:ffi';
import 'dart:io';
import 'dart:isolate';
import 'package:appflowy_backend/rust_stream.dart'; import 'package:appflowy_backend/rust_stream.dart';
import 'package:ffi/ffi.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/services.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 'package:logger/logger.dart';
import 'ffi.dart' as ffi;
export 'package:async/async.dart';
enum ExceptionType { enum ExceptionType {
AppearanceSettingsIsEmpty, AppearanceSettingsIsEmpty,
} }
@ -72,7 +75,8 @@ class RustLogStreamReceiver {
lineLength: 120, // width of the output lineLength: 120, // width of the output
colors: false, // Colorful log messages colors: false, // Colorful log messages
printEmojis: false, // Print an emoji for each log message 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, level: kDebugMode ? Level.trace : Level.info,
); );

View File

@ -1,5 +1,6 @@
// ignore: import_of_legacy_library_into_null_safe // ignore: import_of_legacy_library_into_null_safe
import 'dart:ffi'; import 'dart:ffi';
import 'package:ffi/ffi.dart' as ffi; import 'package:ffi/ffi.dart' as ffi;
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:logger/logger.dart'; import 'package:logger/logger.dart';
@ -14,13 +15,15 @@ class Log {
Log() { Log() {
_logger = Logger( _logger = Logger(
printer: PrettyPrinter( printer: PrettyPrinter(
methodCount: 2, // number of method calls to be displayed methodCount: 2, // number of method calls to be displayed
errorMethodCount: 8, // number of method calls if stacktrace is provided errorMethodCount:
lineLength: 120, // width of the output 8, // number of method calls if stacktrace is provided
colors: true, // Colorful log messages lineLength: 120, // width of the output
printEmojis: true, // Print an emoji for each log message colors: true, // Colorful log messages
printTime: false, // Should each log print contain a timestamp 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, level: kDebugMode ? Level.trace : Level.info,
); );
} }

View File

@ -15,7 +15,7 @@ dependencies:
isolates: ^3.0.3+8 isolates: ^3.0.3+8
protobuf: ^3.1.0 protobuf: ^3.1.0
freezed_annotation: freezed_annotation:
logger: ^2.0.0 logger: ^2.4.0
plugin_platform_interface: ^2.1.3 plugin_platform_interface: ^2.1.3
json_annotation: ^4.7.0 json_annotation: ^4.7.0
appflowy_result: appflowy_result:

View File

@ -1251,13 +1251,13 @@ packages:
source: hosted source: hosted
version: "0.1.5" version: "0.1.5"
logger: logger:
dependency: transitive dependency: "direct main"
description: description:
name: logger name: logger
sha256: "6bbb9d6f7056729537a4309bda2e74e18e5d9f14302489cc1e93f33b3fe32cac" sha256: "697d067c60c20999686a0add96cf6aba723b3aa1f83ecf806a8097231529ec32"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.0.2+1" version: "2.4.0"
logging: logging:
dependency: transitive dependency: transitive
description: description:

View File

@ -144,6 +144,7 @@ dependencies:
markdown_widget: ^2.3.2+6 markdown_widget: ^2.3.2+6
desktop_drop: ^0.4.4 desktop_drop: ^0.4.4
markdown: markdown:
logger: ^2.4.0
# Window Manager for MacOS and Linux # Window Manager for MacOS and Linux
window_manager: ^0.3.9 window_manager: ^0.3.9

View File

@ -494,10 +494,10 @@
}, },
"cache": { "cache": {
"title": "Clear 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": { "dialog": {
"title": "Are you sure?", "title": "Clear cache",
"description": "Clearing the cache will cause images and fonts to be re-downloaded on load. This action will not remove or modify 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.",
"successHint": "Cache cleared!" "successHint": "Cache cleared!"
} }
}, },
@ -757,7 +757,7 @@
"freeLabels": { "freeLabels": {
"itemOne": "charged per workspace", "itemOne": "charged per workspace",
"itemTwo": "3", "itemTwo": "3",
"itemThree": "", "itemThree": " ",
"itemFour": "0", "itemFour": "0",
"itemFive": "5 GB", "itemFive": "5 GB",
"itemSix": "yes", "itemSix": "yes",
@ -767,7 +767,7 @@
"proLabels": { "proLabels": {
"itemOne": "charged per workspace", "itemOne": "charged per workspace",
"itemTwo": "up to 10", "itemTwo": "up to 10",
"itemThree": "", "itemThree": " ",
"itemFour": "10 guests billed as one seat", "itemFour": "10 guests billed as one seat",
"itemFive": "unlimited", "itemFive": "unlimited",
"itemSix": "yes", "itemSix": "yes",

View File

@ -235,7 +235,6 @@ pub(crate) fn notify_did_update_section_views(workspace_id: &str, folder: &Folde
private_views.len() private_views.len()
); );
// TODO(Lucas.xu) - Only notify the section changed, not the public/private both.
// Notify the public views // Notify the public views
send_notification(workspace_id, FolderNotification::DidUpdateSectionViews) send_notification(workspace_id, FolderNotification::DidUpdateSectionViews)
.payload(SectionViewsPB { .payload(SectionViewsPB {