Skip to content

Commit

Permalink
feat: support moving page across spaces (AppFlowy-IO#5618)
Browse files Browse the repository at this point in the history
* feat: support moving page across spaces

* feat: refacotor move api

* feat: filter the database views

* feat: support searching in move page menu
  • Loading branch information
LucasXu0 authored Jun 25, 2024
1 parent b9ad276 commit a8ed930
Show file tree
Hide file tree
Showing 11 changed files with 642 additions and 216 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ part 'folder_bloc.freezed.dart';
enum FolderSpaceType {
favorite,
private,
public;
public,
unknown;

ViewSectionPB get toViewSectionPB {
switch (this) {
Expand All @@ -21,6 +22,7 @@ enum FolderSpaceType {
case FolderSpaceType.public:
return ViewSectionPB.Public;
case FolderSpaceType.favorite:
case FolderSpaceType.unknown:
throw UnimplementedError();
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,7 @@ class SpaceBloc extends Bloc<SpaceEvent, SpaceState> {

late WorkspaceService _workspaceService;
String? _workspaceId;
late UserProfilePB userProfile;
WorkspaceSectionsListener? _listener;

@override
Expand Down Expand Up @@ -401,6 +402,7 @@ class SpaceBloc extends Bloc<SpaceEvent, SpaceState> {
void _initial(UserProfilePB userProfile, String workspaceId) {
_workspaceService = WorkspaceService(workspaceId: workspaceId);
_workspaceId = workspaceId;
this.userProfile = userProfile;

_listener = WorkspaceSectionsListener(
user: userProfile,
Expand Down Expand Up @@ -461,7 +463,7 @@ class SpaceBloc extends Bloc<SpaceEvent, SpaceState> {

Future<bool> _getSpaceExpandStatus(ViewPB? space) async {
if (space == null) {
return false;
return true;
}

return getIt<KeyValueStorage>().get(KVKeys.expandedViews).then((result) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import 'package:appflowy/workspace/application/view/view_service.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';
import 'package:freezed_annotation/freezed_annotation.dart';

part 'space_search_bloc.freezed.dart';

class SpaceSearchBloc extends Bloc<SpaceSearchEvent, SpaceSearchState> {
SpaceSearchBloc() : super(SpaceSearchState.initial()) {
on<SpaceSearchEvent>(
(event, emit) async {
await event.when(
initial: () async {
_allViews = await ViewBackendService.getAllViews().fold(
(s) => s.items,
(_) => <ViewPB>[],
);
},
search: (query) {
if (query.isEmpty) {
emit(
state.copyWith(
queryResults: null,
),
);
} else {
final queryResults = _allViews.where(
(view) => view.name.toLowerCase().contains(query.toLowerCase()),
);
emit(
state.copyWith(
queryResults: queryResults.toList(),
),
);
}
},
);
},
);
}

late final List<ViewPB> _allViews;
}

@freezed
class SpaceSearchEvent with _$SpaceSearchEvent {
const factory SpaceSearchEvent.initial() = _Initial;
const factory SpaceSearchEvent.search(String query) = _Search;
}

@freezed
class SpaceSearchState with _$SpaceSearchState {
const factory SpaceSearchState({
List<ViewPB>? queryResults,
}) = _SpaceSearchState;

factory SpaceSearchState.initial() => const SpaceSearchState();
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@ import 'package:appflowy/workspace/application/view/view_ext.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/favorites/favorite_menu_bloc.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/favorites/favorite_more_actions.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/favorites/favorite_pin_action.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/shared_widget.dart';
import 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.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/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';

Expand Down Expand Up @@ -41,7 +41,7 @@ class FavoriteMenu extends StatelessWidget {
mainAxisSize: MainAxisSize.min,
children: [
const VSpace(4),
_FavoriteSearchField(
SpaceSearchField(
width: minWidth - 2 * _kHorizontalPadding,
onSearch: (context, text) {
context
Expand Down Expand Up @@ -197,72 +197,3 @@ class _FavoriteGroups extends StatelessWidget {
];
}
}

class _FavoriteSearchField extends StatefulWidget {
const _FavoriteSearchField({
required this.width,
required this.onSearch,
});

final double width;
final void Function(BuildContext context, String text) onSearch;

@override
State<_FavoriteSearchField> createState() => _FavoriteSearchFieldState();
}

class _FavoriteSearchFieldState extends State<_FavoriteSearchField> {
final focusNode = FocusNode();

@override
void initState() {
super.initState();
focusNode.requestFocus();
}

@override
void dispose() {
focusNode.dispose();
super.dispose();
}

@override
Widget build(BuildContext context) {
return Container(
height: 30,
width: widget.width,
clipBehavior: Clip.antiAlias,
decoration: ShapeDecoration(
shape: RoundedRectangleBorder(
side: const BorderSide(
width: 1.20,
strokeAlign: BorderSide.strokeAlignOutside,
color: Color(0xFF00BCF0),
),
borderRadius: BorderRadius.circular(8),
),
),
child: CupertinoSearchTextField(
onChanged: (text) => widget.onSearch(context, text),
padding: EdgeInsets.zero,
focusNode: focusNode,
placeholder: LocaleKeys.search_label.tr(),
prefixIcon: const FlowySvg(FlowySvgs.m_search_m),
prefixInsets: const EdgeInsets.only(left: 12.0, right: 8.0),
suffixIcon: const Icon(Icons.close),
suffixInsets: const EdgeInsets.only(right: 8.0),
itemSize: 16.0,
decoration: const BoxDecoration(
color: Colors.transparent,
),
placeholderStyle: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Theme.of(context).hintColor,
fontWeight: FontWeight.w400,
),
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.w400,
),
),
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
import 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart';
import 'package:appflowy/workspace/application/sidebar/space/space_bloc.dart';
import 'package:appflowy/workspace/application/sidebar/space/space_search_bloc.dart';
import 'package:appflowy/workspace/application/view/view_ext.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/shared_widget.dart';
import 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';

class MovePageMenu extends StatefulWidget {
const MovePageMenu({
super.key,
required this.sourceView,
required this.userProfile,
required this.workspaceId,
required this.onSelected,
});

final ViewPB sourceView;
final UserProfilePB userProfile;
final String workspaceId;
final void Function(ViewPB view) onSelected;

@override
State<MovePageMenu> createState() => _MovePageMenuState();
}

class _MovePageMenuState extends State<MovePageMenu> {
final isExpandedNotifier = PropertyValueNotifier(true);
final isHoveredNotifier = ValueNotifier(true);

@override
void dispose() {
isExpandedNotifier.dispose();
isHoveredNotifier.dispose();
super.dispose();
}

@override
Widget build(BuildContext context) {
return MultiBlocProvider(
providers: [
BlocProvider(
create: (context) => SpaceBloc()
..add(
SpaceEvent.initial(
widget.userProfile,
widget.workspaceId,
openFirstPage: false,
),
),
),
BlocProvider(
create: (context) => SpaceSearchBloc()
..add(
const SpaceSearchEvent.initial(),
),
),
],
child: BlocBuilder<SpaceBloc, SpaceState>(
builder: (context, state) {
final space = state.currentSpace;
if (space == null) {
return const SizedBox.shrink();
}
return Column(
children: [
SpaceSearchField(
width: 240,
onSearch: (context, value) {
context.read<SpaceSearchBloc>().add(
SpaceSearchEvent.search(
value,
),
);
},
),
const VSpace(10),
BlocBuilder<SpaceSearchBloc, SpaceSearchState>(
builder: (context, state) {
if (state.queryResults == null) {
return Expanded(
child: _buildSpace(space),
);
}
return Expanded(
child: _buildGroupedViews(state.queryResults!),
);
},
),
],
);
},
),
);
}

Widget _buildGroupedViews(List<ViewPB> views) {
final groupedViews = views
.where(
(view) =>
!_shouldIgnoreView(view, widget.sourceView) && !view.isSpace,
)
.toList();
return _MovePageGroupedViews(
views: groupedViews,
onSelected: widget.onSelected,
);
}

Column _buildSpace(ViewPB space) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SpacePopup(
child: CurrentSpace(
space: space,
),
),
Expanded(
child: SingleChildScrollView(
physics: const ClampingScrollPhysics(),
child: SpacePages(
key: ValueKey(space.id),
space: space,
isHovered: isHoveredNotifier,
isExpandedNotifier: isExpandedNotifier,
shouldIgnoreView: (view) => _shouldIgnoreView(
view,
widget.sourceView,
),
// hide the hover status and disable the editing actions
disableSelectedStatus: true,
// hide the ... and + buttons
rightIconsBuilder: (context, view) => [],
onSelected: (_, view) => widget.onSelected(view),
),
),
),
],
);
}
}

class _MovePageGroupedViews extends StatelessWidget {
const _MovePageGroupedViews({
required this.views,
required this.onSelected,
});

final List<ViewPB> views;
final void Function(ViewPB view) onSelected;

@override
Widget build(BuildContext context) {
return SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children: views
.map(
(e) => ViewItem(
key: ValueKey(e.id),
view: e,
spaceType: FolderSpaceType.unknown,
level: 0,
onSelected: (_, view) => onSelected(view),
isFeedback: false,
isDraggable: false,
shouldRenderChildren: false,
leftIconBuilder: (_, __) => const HSpace(0.0),
rightIconsBuilder: (_, view) => [],
),
)
.toList(),
),
);
}
}

bool _shouldIgnoreView(ViewPB view, ViewPB sourceView) {
// ignore the source view and database view, don't render it in the list.
if (view.layout != ViewLayoutPB.Document) {
return true;
}
return view.id == sourceView.id;
}
Loading

0 comments on commit a8ed930

Please sign in to comment.