mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2026-01-06 04:11:53 +00:00
feat: enable to reorder favorites (#7172)
This commit is contained in:
parent
c3b702849f
commit
99a4e330e8
@ -196,5 +196,58 @@ void main() {
|
||||
await tester.pumpAndSettle();
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets(
|
||||
'reorder favorites',
|
||||
(tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
|
||||
/// there are no favorite views
|
||||
final favorites = find.descendant(
|
||||
of: find.byType(FavoriteFolder),
|
||||
matching: find.byType(ViewItem),
|
||||
);
|
||||
expect(favorites, findsNothing);
|
||||
|
||||
/// create views and then favorite them
|
||||
const pageNames = ['001', '002', '003'];
|
||||
for (final name in pageNames) {
|
||||
await tester.createNewPageWithNameUnderParent(name: name);
|
||||
}
|
||||
for (final name in pageNames) {
|
||||
await tester.favoriteViewByName(name);
|
||||
}
|
||||
expect(favorites, findsNWidgets(pageNames.length));
|
||||
|
||||
final oldNames = favorites
|
||||
.evaluate()
|
||||
.map((e) => (e.widget as ViewItem).view.name)
|
||||
.toList();
|
||||
expect(oldNames, pageNames);
|
||||
|
||||
/// drag first to last
|
||||
await tester.reorderFavorite(
|
||||
fromName: '001',
|
||||
toName: '003',
|
||||
);
|
||||
List<String> newNames = favorites
|
||||
.evaluate()
|
||||
.map((e) => (e.widget as ViewItem).view.name)
|
||||
.toList();
|
||||
expect(newNames, ['002', '003', '001']);
|
||||
|
||||
/// drag first to second
|
||||
await tester.reorderFavorite(
|
||||
fromName: '002',
|
||||
toName: '003',
|
||||
);
|
||||
newNames = favorites
|
||||
.evaluate()
|
||||
.map((e) => (e.widget as ViewItem).view.name)
|
||||
.toList();
|
||||
expect(newNames, ['003', '002', '001']);
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@ -19,6 +19,7 @@ import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy/user/presentation/screens/screens.dart';
|
||||
import 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/widgets.dart';
|
||||
import 'package:appflowy/workspace/application/view/view_ext.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/favorites/favorite_folder.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/footer/sidebar_footer.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/shared/sidebar_new_page_button.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/shared_widget.dart';
|
||||
@ -600,6 +601,23 @@ extension CommonOperations on WidgetTester {
|
||||
await pumpAndSettle();
|
||||
}
|
||||
|
||||
Future<void> reorderFavorite({
|
||||
required String fromName,
|
||||
required String toName,
|
||||
}) async {
|
||||
final from = find.descendant(
|
||||
of: find.byType(FavoriteFolder),
|
||||
matching: find.text(fromName),
|
||||
),
|
||||
to = find.descendant(
|
||||
of: find.byType(FavoriteFolder),
|
||||
matching: find.text(toName),
|
||||
);
|
||||
final distanceY = getCenter(to).dy - getCenter(from).dx;
|
||||
await drag(from, Offset(0, distanceY));
|
||||
await pumpAndSettle();
|
||||
}
|
||||
|
||||
// tap the button with [FlowySvgData]
|
||||
Future<void> tapButtonWithFlowySvgData(FlowySvgData svg) async {
|
||||
final button = find.byWidgetPredicate(
|
||||
|
||||
@ -18,6 +18,7 @@ class FavoriteBloc extends Bloc<FavoriteEvent, FavoriteState> {
|
||||
|
||||
final _service = FavoriteService();
|
||||
final _listener = FavoriteListener();
|
||||
bool isReordering = false;
|
||||
|
||||
@override
|
||||
Future<void> close() async {
|
||||
@ -68,10 +69,7 @@ class FavoriteBloc extends Bloc<FavoriteEvent, FavoriteState> {
|
||||
await _service.pinFavorite(view);
|
||||
}
|
||||
|
||||
await _service.toggleFavorite(
|
||||
view.id,
|
||||
!view.isFavorite,
|
||||
);
|
||||
await _service.toggleFavorite(view.id);
|
||||
},
|
||||
pin: (view) async {
|
||||
await _service.pinFavorite(view);
|
||||
@ -81,6 +79,21 @@ class FavoriteBloc extends Bloc<FavoriteEvent, FavoriteState> {
|
||||
await _service.unpinFavorite(view);
|
||||
add(const FavoriteEvent.fetchFavorites());
|
||||
},
|
||||
reorder: (oldIndex, newIndex) async {
|
||||
/// TODO: this is a workaround to reorder the favorite views
|
||||
isReordering = true;
|
||||
final pinnedViews = state.pinnedViews.toList();
|
||||
if (oldIndex < newIndex) newIndex -= 1;
|
||||
final target = pinnedViews.removeAt(oldIndex);
|
||||
pinnedViews.insert(newIndex, target);
|
||||
emit(state.copyWith(pinnedViews: pinnedViews));
|
||||
for (final view in pinnedViews) {
|
||||
await _service.toggleFavorite(view.item.id);
|
||||
await _service.toggleFavorite(view.item.id);
|
||||
}
|
||||
add(const FavoriteEvent.fetchFavorites());
|
||||
isReordering = false;
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
@ -90,20 +103,29 @@ class FavoriteBloc extends Bloc<FavoriteEvent, FavoriteState> {
|
||||
FlowyResult<RepeatedViewPB, FlowyError> favoriteOrFailed,
|
||||
bool didFavorite,
|
||||
) {
|
||||
favoriteOrFailed.fold(
|
||||
(favorite) => add(const FetchFavorites()),
|
||||
(error) => Log.error(error),
|
||||
);
|
||||
if (!isReordering) {
|
||||
favoriteOrFailed.fold(
|
||||
(favorite) => add(const FetchFavorites()),
|
||||
(error) => Log.error(error),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@freezed
|
||||
class FavoriteEvent with _$FavoriteEvent {
|
||||
const factory FavoriteEvent.initial() = Initial;
|
||||
|
||||
const factory FavoriteEvent.toggle(ViewPB view) = ToggleFavorite;
|
||||
|
||||
const factory FavoriteEvent.fetchFavorites() = FetchFavorites;
|
||||
|
||||
const factory FavoriteEvent.pin(ViewPB view) = PinFavorite;
|
||||
|
||||
const factory FavoriteEvent.unpin(ViewPB view) = UnpinFavorite;
|
||||
|
||||
const factory FavoriteEvent.reorder(int oldIndex, int newIndex) =
|
||||
ReorderFavorite;
|
||||
}
|
||||
|
||||
@freezed
|
||||
|
||||
@ -25,10 +25,7 @@ class FavoriteService {
|
||||
});
|
||||
}
|
||||
|
||||
Future<FlowyResult<void, FlowyError>> toggleFavorite(
|
||||
String viewId,
|
||||
bool favoriteStatus,
|
||||
) async {
|
||||
Future<FlowyResult<void, FlowyError>> toggleFavorite(String viewId) async {
|
||||
final id = RepeatedViewIdPB.create()..items.add(viewId);
|
||||
return FolderEventToggleFavorite(id).send();
|
||||
}
|
||||
|
||||
@ -3,6 +3,7 @@ import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/view/view_bloc.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/home_sizes.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/favorites/favorite_menu.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/favorites/favorite_more_actions.dart';
|
||||
@ -54,8 +55,7 @@ class _FavoriteFolderState extends State<FavoriteFolder> {
|
||||
.read<FolderBloc>()
|
||||
.add(const FolderEvent.expandOrUnExpand()),
|
||||
),
|
||||
// pages
|
||||
..._buildViews(context, state, isHovered),
|
||||
buildReorderListView(context, state),
|
||||
if (state.isExpanded) ...[
|
||||
// more button
|
||||
const VSpace(2),
|
||||
@ -69,51 +69,91 @@ class _FavoriteFolderState extends State<FavoriteFolder> {
|
||||
);
|
||||
}
|
||||
|
||||
Iterable<Widget> _buildViews(
|
||||
Widget buildReorderListView(
|
||||
BuildContext context,
|
||||
FolderState state,
|
||||
ValueNotifier<bool> isHovered,
|
||||
) {
|
||||
if (!state.isExpanded) {
|
||||
return [];
|
||||
if (!state.isExpanded) return const SizedBox.shrink();
|
||||
|
||||
final favoriteBloc = context.read<FavoriteBloc>();
|
||||
final pinnedViews =
|
||||
favoriteBloc.state.pinnedViews.map((e) => e.item).toList();
|
||||
|
||||
if (pinnedViews.isEmpty) return const SizedBox.shrink();
|
||||
if (pinnedViews.length == 1) {
|
||||
return buildViewItem(pinnedViews.first);
|
||||
}
|
||||
|
||||
final pinnedViews =
|
||||
context.read<FavoriteBloc>().state.pinnedViews.map((e) => e.item);
|
||||
|
||||
return pinnedViews.map(
|
||||
(view) => ViewItem(
|
||||
key: ValueKey('${FolderSpaceType.favorite.name} ${view.id}'),
|
||||
spaceType: FolderSpaceType.favorite,
|
||||
isDraggable: false,
|
||||
isFirstChild: view.id == widget.views.first.id,
|
||||
isFeedback: false,
|
||||
view: view,
|
||||
enableRightClickContext: true,
|
||||
leftPadding: HomeSpaceViewSizes.leftPadding,
|
||||
leftIconBuilder: (_, __) =>
|
||||
const HSpace(HomeSpaceViewSizes.leftPadding),
|
||||
level: 0,
|
||||
isHovered: isHovered,
|
||||
rightIconsBuilder: (context, view) => [
|
||||
FavoriteMoreActions(view: view),
|
||||
const HSpace(8.0),
|
||||
FavoritePinAction(view: view),
|
||||
const HSpace(4.0),
|
||||
],
|
||||
shouldRenderChildren: false,
|
||||
shouldLoadChildViews: false,
|
||||
onTertiarySelected: (_, view) => context.read<TabsBloc>().openTab(view),
|
||||
onSelected: (_, view) {
|
||||
if (HardwareKeyboard.instance.isControlPressed) {
|
||||
context.read<TabsBloc>().openTab(view);
|
||||
}
|
||||
|
||||
context.read<TabsBloc>().openPlugin(view);
|
||||
return Theme(
|
||||
data: Theme.of(context).copyWith(
|
||||
canvasColor: Colors.transparent,
|
||||
shadowColor: Colors.transparent,
|
||||
),
|
||||
child: ReorderableListView.builder(
|
||||
shrinkWrap: true,
|
||||
buildDefaultDragHandles: false,
|
||||
itemCount: pinnedViews.length,
|
||||
padding: EdgeInsets.zero,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemBuilder: (context, i) {
|
||||
final view = pinnedViews[i];
|
||||
return ReorderableDragStartListener(
|
||||
key: ValueKey(view.id),
|
||||
index: i,
|
||||
child: DecoratedBox(
|
||||
decoration: const BoxDecoration(color: Colors.transparent),
|
||||
child: buildViewItem(view),
|
||||
),
|
||||
);
|
||||
},
|
||||
onReorder: (oldIndex, newIndex) {
|
||||
favoriteBloc.add(FavoriteEvent.reorder(oldIndex, newIndex));
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildViewItem(ViewPB view) {
|
||||
return ViewItem(
|
||||
key: ValueKey('${FolderSpaceType.favorite.name} ${view.id}'),
|
||||
spaceType: FolderSpaceType.favorite,
|
||||
isDraggable: false,
|
||||
isFirstChild: view.id == widget.views.first.id,
|
||||
isFeedback: false,
|
||||
view: view,
|
||||
enableRightClickContext: true,
|
||||
leftPadding: HomeSpaceViewSizes.leftPadding,
|
||||
leftIconBuilder: (_, __) => const HSpace(HomeSpaceViewSizes.leftPadding),
|
||||
level: 0,
|
||||
isHovered: isHovered,
|
||||
rightIconsBuilder: (context, view) => [
|
||||
Listener(
|
||||
child: FavoriteMoreActions(view: view),
|
||||
onPointerDown: (e) {
|
||||
context.read<ViewBloc>().add(const ViewEvent.setIsEditing(true));
|
||||
},
|
||||
),
|
||||
const HSpace(8.0),
|
||||
Listener(
|
||||
child: FavoritePinAction(view: view),
|
||||
onPointerDown: (e) {
|
||||
context.read<ViewBloc>().add(const ViewEvent.setIsEditing(true));
|
||||
},
|
||||
),
|
||||
const HSpace(4.0),
|
||||
],
|
||||
shouldRenderChildren: false,
|
||||
shouldLoadChildViews: false,
|
||||
onTertiarySelected: (_, view) => context.read<TabsBloc>().openTab(view),
|
||||
onSelected: (_, view) {
|
||||
if (HardwareKeyboard.instance.isControlPressed) {
|
||||
context.read<TabsBloc>().openTab(view);
|
||||
}
|
||||
|
||||
context.read<TabsBloc>().openPlugin(view);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class FavoriteHeader extends StatelessWidget {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user