Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: hide breadcrumbs for deleted pages and show trash #6512

Merged
merged 7 commits into from
Oct 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_page_block.dart';
import 'package:appflowy/plugins/inline_actions/widgets/inline_actions_handler.dart';
import 'package:appflowy/workspace/presentation/widgets/view_title_bar.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';

import '../../shared/util.dart';

import 'document_inline_page_reference_test.dart';

void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();

group('Document deletion', () {
testWidgets('Trash breadcrumb', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();

// This test shares behavior with the inline page reference test, thus
// we utilize the same helper functions there.
final name = await createDocumentToReference(tester);
await tester.editor.tapLineOfEditorAt(0);
await tester.pumpAndSettle();

await triggerReferenceDocumentBySlashMenu(tester);

// Search for prefix of document
await enterDocumentText(tester);

// Select result
final optionFinder = find.descendant(
of: find.byType(InlineActionsHandler),
matching: find.text(name),
);

await tester.tap(optionFinder);
await tester.pumpAndSettle();

final mentionBlock = find.byType(MentionPageBlock);
expect(mentionBlock, findsOneWidget);

// Delete the page
await tester.hoverOnPageName(name);
await tester.tapDeletePageButton();
await tester.pumpAndSettle();

// Navigate to the deleted page from the inline mention
await tester.tap(mentionBlock);
await tester.pumpUntilFound(find.byType(TrashBreadcrumb));

expect(find.byType(TrashBreadcrumb), findsOneWidget);

// Navigate using the trash breadcrumb
await tester.tap(
find.descendant(
of: find.byType(TrashBreadcrumb),
matching: find.text(
LocaleKeys.trash_text.tr(),
),
),
);
await tester.pumpUntilFound(find.text(LocaleKeys.trash_restoreAll.tr()));

// Restore all
await tester.tap(find.text(LocaleKeys.trash_restoreAll.tr()));
await tester.pumpAndSettle();
await tester.tap(find.text(LocaleKeys.trash_restore.tr()));
await tester.pumpAndSettle();

// Navigate back to the document
await tester.openPage('Getting started');
await tester.pumpAndSettle();

await tester.tap(mentionBlock);
await tester.pumpAndSettle();

expect(find.byType(TrashBreadcrumb), findsNothing);
});
});
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import 'package:integration_test/integration_test.dart';

import 'document_app_lifecycle_test.dart' as document_app_lifecycle_test;
import 'document_title_test.dart' as document_title_test;
import 'document_deletion_test.dart' as document_deletion_test;

void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
Expand All @@ -11,4 +12,5 @@ void main() {
// Disable subPage test temporarily, enable it in version 0.7.2
// document_sub_page_test.main();
document_app_lifecycle_test.main();
document_deletion_test.main();
}
Original file line number Diff line number Diff line change
Expand Up @@ -279,12 +279,14 @@ extension CommonOperations on WidgetTester {

/// Tap the delete permanently button.
///
/// the restore button will show after the current page is deleted.
/// the delete permanently button will show after the current page is deleted.
Future<void> tapDeletePermanentlyButton() async {
final restoreButton = find.textContaining(
final deleteButton = find.textContaining(
LocaleKeys.deletePagePrompt_deletePermanent.tr(),
);
await tapButton(restoreButton);
await tapButton(deleteButton);
await tap(find.text(LocaleKeys.button_delete.tr()));
await pumpAndSettle();
}

/// Tap the share button above the document page.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,7 @@ class _DatabaseDocumentPageState extends State<DatabaseDocumentPage> {

Widget _buildBanner(BuildContext context) {
return DocumentBanner(
viewName: widget.view.name,
onRestore: () => context.read<DocumentBloc>().add(
const DocumentEvent.restorePage(),
),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,7 @@ class _DocumentPageState extends State<DocumentPage>

Widget buildBanner(BuildContext context) {
return DocumentBanner(
viewName: widget.view.name,
onRestore: () =>
context.read<DocumentBloc>().add(const DocumentEvent.restorePage()),
onDelete: () => context
Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,21 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/size.dart';
import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flowy_infra_ui/widget/buttons/base_styled_button.dart';
import 'package:flowy_infra_ui/widget/spacing.dart';
import 'package:flutter/material.dart';
import 'package:appflowy/generated/locale_keys.g.dart';

class DocumentBanner extends StatelessWidget {
const DocumentBanner({
super.key,
required this.viewName,
required this.onRestore,
required this.onDelete,
});

final String viewName;
final void Function() onRestore;
final void Function() onDelete;

Expand Down Expand Up @@ -58,7 +61,16 @@ class DocumentBanner extends StatelessWidget {
highlightColor: Theme.of(context).colorScheme.error,
outlineColor: colorScheme.tertiaryContainer,
borderRadius: Corners.s8Border,
onPressed: onDelete,
onPressed: () => showConfirmDeletionDialog(
context: context,
name: viewName.trim().isEmpty
? LocaleKeys.menuAppHeader_defaultNewPageName.tr()
: viewName,
description: LocaleKeys
.deletePagePrompt_deletePermanentDescription
.tr(),
onConfirm: onDelete,
),
child: FlowyText.medium(
LocaleKeys.deletePagePrompt_deletePermanent.tr(),
color: colorScheme.tertiary,
Expand Down
7 changes: 5 additions & 2 deletions frontend/appflowy_flutter/lib/plugins/trash/trash_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -169,8 +169,11 @@ class _TrashPageState extends State<TrashPage> {
),
onDelete: () => showConfirmDeletionDialog(
context: context,
name: object.name,
description: LocaleKeys.deletePagePrompt_deletePermanent.tr(),
name: object.name.trim().isEmpty
? LocaleKeys.menuAppHeader_defaultNewPageName.tr()
: object.name,
description:
LocaleKeys.deletePagePrompt_deletePermanentDescription.tr(),
onConfirm: () =>
context.read<TrashBloc>().add(TrashEvent.delete(object)),
),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import 'package:appflowy/plugins/trash/application/prelude.dart';
import 'package:appflowy/workspace/application/view/prelude.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/trash.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:appflowy_result/appflowy_result.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
Expand All @@ -7,14 +9,19 @@ import 'package:freezed_annotation/freezed_annotation.dart';
part 'view_title_bar_bloc.freezed.dart';

class ViewTitleBarBloc extends Bloc<ViewTitleBarEvent, ViewTitleBarState> {
ViewTitleBarBloc({
required this.view,
}) : super(ViewTitleBarState.initial()) {
viewListener = ViewListener(
viewId: view.id,
)..start(
onViewChildViewsUpdated: (p0) {
add(const ViewTitleBarEvent.reload());
ViewTitleBarBloc({required this.view}) : super(ViewTitleBarState.initial()) {
trashService = TrashService();
viewListener = ViewListener(viewId: view.id)
..start(
onViewChildViewsUpdated: (_) => add(const ViewTitleBarEvent.reload()),
);
trashListener = TrashListener()
..start(
trashUpdated: (trashOrFailed) {
final trash = trashOrFailed.toNullable();
if (trash != null) {
add(ViewTitleBarEvent.trashUpdated(trash: trash));
}
},
);

Expand All @@ -30,18 +37,32 @@ class ViewTitleBarBloc extends Bloc<ViewTitleBarEvent, ViewTitleBarState> {
(s) => s.items,
(f) => [],
);
emit(state.copyWith(ancestors: ancestors));

final isDeleted = (await trashService.readTrash()).fold(
(s) => s.items.any((t) => t.id == view.id),
(f) => false,
);

emit(state.copyWith(ancestors: ancestors, isDeleted: isDeleted));
},
trashUpdated: (trash) {
if (trash.any((t) => t.id == view.id)) {
emit(state.copyWith(isDeleted: true));
}
},
);
},
);
}

final ViewPB view;
late final TrashService trashService;
late final ViewListener viewListener;
late final TrashListener trashListener;

@override
Future<void> close() {
trashListener.close();
viewListener.stop();
return super.close();
}
Expand All @@ -51,12 +72,16 @@ class ViewTitleBarBloc extends Bloc<ViewTitleBarEvent, ViewTitleBarState> {
class ViewTitleBarEvent with _$ViewTitleBarEvent {
const factory ViewTitleBarEvent.initial() = Initial;
const factory ViewTitleBarEvent.reload() = Reload;
const factory ViewTitleBarEvent.trashUpdated({
required List<TrashPB> trash,
}) = TrashUpdated;
}

@freezed
class ViewTitleBarState with _$ViewTitleBarState {
const factory ViewTitleBarState({
required List<ViewPB> ancestors,
@Default(false) bool isDeleted,
}) = _ViewTitleBarState;

factory ViewTitleBarState.initial() => const ViewTitleBarState(ancestors: []);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,20 @@
import 'package:flutter/material.dart';

import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/startup/plugin/plugin.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart';
import 'package:appflowy/workspace/application/view/view_ext.dart';
import 'package:appflowy/workspace/application/view_title/view_title_bar_bloc.dart';
import 'package:appflowy/workspace/application/view_title/view_title_bloc.dart';
import 'package:appflowy/workspace/presentation/home/menu/menu_shared_state.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/space_icon.dart';
import 'package:appflowy/workspace/presentation/widgets/rename_view_popover.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';

// space name > ... > view_title
Expand All @@ -22,7 +26,6 @@ class ViewTitleBar extends StatelessWidget {

final ViewPB view;

// late Future<List<ViewPB>> ancestors;
@override
Widget build(BuildContext context) {
return BlocProvider(
Expand All @@ -39,7 +42,11 @@ class ViewTitleBar extends StatelessWidget {
child: SizedBox(
height: 24,
child: Row(
children: _buildViewTitles(context, ancestors),
children: _buildViewTitles(
context,
ancestors,
state.isDeleted,
),
),
),
);
Expand All @@ -48,7 +55,15 @@ class ViewTitleBar extends StatelessWidget {
);
}

List<Widget> _buildViewTitles(BuildContext context, List<ViewPB> views) {
List<Widget> _buildViewTitles(
BuildContext context,
List<ViewPB> views,
bool isDeleted,
) {
if (isDeleted) {
return _buildDeletedTitle(context, views.last);
}

// if the level is too deep, only show the last two view, the first one view and the root view
// for example:
// if the views are [root, view1, view2, view3, view4, view5], only show [root, view1, ..., view4, view5]
Expand Down Expand Up @@ -103,6 +118,55 @@ class ViewTitleBar extends StatelessWidget {
}
return children;
}

List<Widget> _buildDeletedTitle(BuildContext context, ViewPB view) {
return [
const TrashBreadcrumb(),
const FlowySvg(FlowySvgs.title_bar_divider_s),
FlowyTooltip(
key: ValueKey(view.id),
message: view.name,
child: ViewTitle(
view: view,
onUpdated: () => context
.read<ViewTitleBarBloc>()
.add(const ViewTitleBarEvent.reload()),
),
),
];
}
}

class TrashBreadcrumb extends StatelessWidget {
const TrashBreadcrumb({
super.key,
});

@override
Widget build(BuildContext context) {
return SizedBox(
height: 32,
child: FlowyButton(
useIntrinsicWidth: true,
onTap: () {
getIt<MenuSharedState>().latestOpenView = null;
getIt<TabsBloc>().add(
TabsEvent.openPlugin(
plugin: makePlugin(pluginType: PluginType.trash),
),
);
},
text: Row(
children: [
const FlowySvg(FlowySvgs.trash_s),
const HSpace(4.0),
FlowyText.regular(LocaleKeys.trash_text.tr()),
const HSpace(4.0),
],
),
),
);
}
}

enum ViewTitleBehavior {
Expand Down
Loading
Loading