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/"
},
]
}
}

View File

@ -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),

View File

@ -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 '';
}

View File

@ -141,7 +141,7 @@ class SpaceBloc extends Bloc<SpaceEvent, SpaceState> {
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<SpaceEvent, SpaceState> {
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<SpaceEvent, SpaceState> {
if (_workspaceId == null) {
return false;
}
try {
final user =
await UserBackendService.getCurrentUserProfile().getOrThrow();
@ -526,6 +528,13 @@ class SpaceBloc extends Bloc<SpaceEvent, SpaceState> {
(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<SpaceEvent, SpaceState> {
(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;
}

View File

@ -143,7 +143,7 @@ class ViewBloc extends Bloc<ViewEvent, ViewState> {
// 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(

View File

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

View File

@ -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<ConfirmPopup> {
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<ConfirmPopup> {
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,
);
}
}

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/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<void> 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<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) {
for (int i = 0; i < views.length; i++) {
final view = views[i];

View File

@ -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<FlowyCacheManager>().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);
);
},
),
],

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: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,
);

View File

@ -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,
);
}

View File

@ -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:

View File

@ -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:

View File

@ -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

View File

@ -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",

View File

@ -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 {