mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
feat: support moving page across spaces (#5618)
* feat: support moving page across spaces * feat: refacotor move api * feat: filter the database views * feat: support searching in move page menu
This commit is contained in:
@ -12,7 +12,8 @@ part 'folder_bloc.freezed.dart';
|
|||||||
enum FolderSpaceType {
|
enum FolderSpaceType {
|
||||||
favorite,
|
favorite,
|
||||||
private,
|
private,
|
||||||
public;
|
public,
|
||||||
|
unknown;
|
||||||
|
|
||||||
ViewSectionPB get toViewSectionPB {
|
ViewSectionPB get toViewSectionPB {
|
||||||
switch (this) {
|
switch (this) {
|
||||||
@ -21,6 +22,7 @@ enum FolderSpaceType {
|
|||||||
case FolderSpaceType.public:
|
case FolderSpaceType.public:
|
||||||
return ViewSectionPB.Public;
|
return ViewSectionPB.Public;
|
||||||
case FolderSpaceType.favorite:
|
case FolderSpaceType.favorite:
|
||||||
|
case FolderSpaceType.unknown:
|
||||||
throw UnimplementedError();
|
throw UnimplementedError();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -310,6 +310,7 @@ class SpaceBloc extends Bloc<SpaceEvent, SpaceState> {
|
|||||||
|
|
||||||
late WorkspaceService _workspaceService;
|
late WorkspaceService _workspaceService;
|
||||||
String? _workspaceId;
|
String? _workspaceId;
|
||||||
|
late UserProfilePB userProfile;
|
||||||
WorkspaceSectionsListener? _listener;
|
WorkspaceSectionsListener? _listener;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -401,6 +402,7 @@ class SpaceBloc extends Bloc<SpaceEvent, SpaceState> {
|
|||||||
void _initial(UserProfilePB userProfile, String workspaceId) {
|
void _initial(UserProfilePB userProfile, String workspaceId) {
|
||||||
_workspaceService = WorkspaceService(workspaceId: workspaceId);
|
_workspaceService = WorkspaceService(workspaceId: workspaceId);
|
||||||
_workspaceId = workspaceId;
|
_workspaceId = workspaceId;
|
||||||
|
this.userProfile = userProfile;
|
||||||
|
|
||||||
_listener = WorkspaceSectionsListener(
|
_listener = WorkspaceSectionsListener(
|
||||||
user: userProfile,
|
user: userProfile,
|
||||||
@ -461,7 +463,7 @@ class SpaceBloc extends Bloc<SpaceEvent, SpaceState> {
|
|||||||
|
|
||||||
Future<bool> _getSpaceExpandStatus(ViewPB? space) async {
|
Future<bool> _getSpaceExpandStatus(ViewPB? space) async {
|
||||||
if (space == null) {
|
if (space == null) {
|
||||||
return false;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return getIt<KeyValueStorage>().get(KVKeys.expandedViews).then((result) {
|
return getIt<KeyValueStorage>().get(KVKeys.expandedViews).then((result) {
|
||||||
|
@ -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();
|
||||||
|
}
|
@ -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_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_more_actions.dart';
|
||||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/favorites/favorite_pin_action.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/workspace/presentation/home/menu/view/view_item.dart';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';
|
import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';
|
||||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||||
import 'package:flutter/cupertino.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
|
||||||
@ -41,7 +41,7 @@ class FavoriteMenu extends StatelessWidget {
|
|||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
const VSpace(4),
|
const VSpace(4),
|
||||||
_FavoriteSearchField(
|
SpaceSearchField(
|
||||||
width: minWidth - 2 * _kHorizontalPadding,
|
width: minWidth - 2 * _kHorizontalPadding,
|
||||||
onSearch: (context, text) {
|
onSearch: (context, text) {
|
||||||
context
|
context
|
||||||
@ -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,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -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;
|
||||||
|
}
|
@ -1,10 +1,21 @@
|
|||||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||||
|
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_bloc.dart';
|
||||||
|
import 'package:appflowy/workspace/application/view/view_bloc.dart';
|
||||||
|
import 'package:appflowy/workspace/application/view/view_ext.dart';
|
||||||
|
import 'package:appflowy/workspace/presentation/home/home_sizes.dart';
|
||||||
|
import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/sidebar_space_menu.dart';
|
||||||
|
import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/space_icon.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-folder/view.pb.dart';
|
||||||
|
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||||
import 'package:flowy_infra_ui/style_widget/decoration.dart';
|
import 'package:flowy_infra_ui/style_widget/decoration.dart';
|
||||||
|
import 'package:flutter/cupertino.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
|
||||||
@ -262,3 +273,207 @@ class DeleteSpacePopup extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class SpacePopup extends StatelessWidget {
|
||||||
|
const SpacePopup({
|
||||||
|
super.key,
|
||||||
|
required this.child,
|
||||||
|
});
|
||||||
|
|
||||||
|
final Widget child;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return SizedBox(
|
||||||
|
height: HomeSizes.workspaceSectionHeight,
|
||||||
|
child: AppFlowyPopover(
|
||||||
|
constraints: const BoxConstraints(maxWidth: 260),
|
||||||
|
direction: PopoverDirection.bottomWithLeftAligned,
|
||||||
|
clickHandler: PopoverClickHandler.gestureDetector,
|
||||||
|
offset: const Offset(0, 4),
|
||||||
|
popupBuilder: (_) => BlocProvider.value(
|
||||||
|
value: context.read<SpaceBloc>(),
|
||||||
|
child: const SidebarSpaceMenu(),
|
||||||
|
),
|
||||||
|
child: FlowyButton(
|
||||||
|
useIntrinsicWidth: true,
|
||||||
|
margin: const EdgeInsets.only(left: 3.0, right: 4.0),
|
||||||
|
iconPadding: 10.0,
|
||||||
|
text: child,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class CurrentSpace extends StatelessWidget {
|
||||||
|
const CurrentSpace({
|
||||||
|
super.key,
|
||||||
|
required this.space,
|
||||||
|
});
|
||||||
|
|
||||||
|
final ViewPB space;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Row(
|
||||||
|
children: [
|
||||||
|
SpaceIcon(
|
||||||
|
dimension: 20,
|
||||||
|
space: space,
|
||||||
|
cornerRadius: 6.0,
|
||||||
|
),
|
||||||
|
const HSpace(10),
|
||||||
|
Flexible(
|
||||||
|
child: FlowyText.medium(
|
||||||
|
space.name,
|
||||||
|
fontSize: 14.0,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const HSpace(4.0),
|
||||||
|
const FlowySvg(
|
||||||
|
FlowySvgs.workspace_drop_down_menu_show_s,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class SpacePages extends StatelessWidget {
|
||||||
|
const SpacePages({
|
||||||
|
super.key,
|
||||||
|
required this.space,
|
||||||
|
required this.isHovered,
|
||||||
|
required this.isExpandedNotifier,
|
||||||
|
required this.onSelected,
|
||||||
|
this.rightIconsBuilder,
|
||||||
|
this.disableSelectedStatus = false,
|
||||||
|
this.onTertiarySelected,
|
||||||
|
this.shouldIgnoreView,
|
||||||
|
});
|
||||||
|
|
||||||
|
final ViewPB space;
|
||||||
|
final ValueNotifier<bool> isHovered;
|
||||||
|
final PropertyValueNotifier<bool> isExpandedNotifier;
|
||||||
|
final bool disableSelectedStatus;
|
||||||
|
final ViewItemRightIconsBuilder? rightIconsBuilder;
|
||||||
|
final ViewItemOnSelected onSelected;
|
||||||
|
final ViewItemOnSelected? onTertiarySelected;
|
||||||
|
final bool Function(ViewPB view)? shouldIgnoreView;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return BlocProvider(
|
||||||
|
create: (context) =>
|
||||||
|
ViewBloc(view: space)..add(const ViewEvent.initial()),
|
||||||
|
child: BlocBuilder<ViewBloc, ViewState>(
|
||||||
|
builder: (context, state) {
|
||||||
|
// filter the child views that should be ignored
|
||||||
|
var childViews = state.view.childViews;
|
||||||
|
if (shouldIgnoreView != null) {
|
||||||
|
childViews = childViews
|
||||||
|
.where((childView) => !shouldIgnoreView!(childView))
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
return Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: childViews
|
||||||
|
.map(
|
||||||
|
(view) => ViewItem(
|
||||||
|
key: ValueKey('${space.id} ${view.id}'),
|
||||||
|
spaceType:
|
||||||
|
space.spacePermission == SpacePermission.publicToAll
|
||||||
|
? FolderSpaceType.public
|
||||||
|
: FolderSpaceType.private,
|
||||||
|
isFirstChild: view.id == childViews.first.id,
|
||||||
|
view: view,
|
||||||
|
level: 0,
|
||||||
|
leftPadding: HomeSpaceViewSizes.leftPadding,
|
||||||
|
isFeedback: false,
|
||||||
|
isHovered: isHovered,
|
||||||
|
disableSelectedStatus: disableSelectedStatus,
|
||||||
|
isExpandedNotifier: isExpandedNotifier,
|
||||||
|
rightIconsBuilder: rightIconsBuilder,
|
||||||
|
onSelected: onSelected,
|
||||||
|
onTertiarySelected: onTertiarySelected,
|
||||||
|
shouldIgnoreView: shouldIgnoreView,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class SpaceSearchField extends StatefulWidget {
|
||||||
|
const SpaceSearchField({
|
||||||
|
super.key,
|
||||||
|
required this.width,
|
||||||
|
required this.onSearch,
|
||||||
|
});
|
||||||
|
|
||||||
|
final double width;
|
||||||
|
final void Function(BuildContext context, String text) onSearch;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<SpaceSearchField> createState() => _SpaceSearchFieldState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SpaceSearchFieldState extends State<SpaceSearchField> {
|
||||||
|
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.magnifier_s),
|
||||||
|
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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1,19 +1,15 @@
|
|||||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||||
import 'package:appflowy/startup/startup.dart';
|
import 'package:appflowy/startup/startup.dart';
|
||||||
import 'package:appflowy/workspace/application/favorite/favorite_bloc.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/sidebar/space/space_bloc.dart';
|
import 'package:appflowy/workspace/application/sidebar/space/space_bloc.dart';
|
||||||
import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart';
|
import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart';
|
||||||
import 'package:appflowy/workspace/application/view/view_bloc.dart';
|
|
||||||
import 'package:appflowy/workspace/application/view/view_ext.dart';
|
|
||||||
import 'package:appflowy/workspace/presentation/home/home_sizes.dart';
|
|
||||||
import 'package:appflowy/workspace/presentation/home/hotkeys.dart';
|
import 'package:appflowy/workspace/presentation/home/hotkeys.dart';
|
||||||
import 'package:appflowy/workspace/presentation/home/menu/menu_shared_state.dart';
|
import 'package:appflowy/workspace/presentation/home/menu/menu_shared_state.dart';
|
||||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/favorites/favorite_folder.dart';
|
import 'package:appflowy/workspace/presentation/home/menu/sidebar/favorites/favorite_folder.dart';
|
||||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/shared/rename_view_dialog.dart';
|
import 'package:appflowy/workspace/presentation/home/menu/sidebar/shared/rename_view_dialog.dart';
|
||||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/create_space_popup.dart';
|
import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/create_space_popup.dart';
|
||||||
|
import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/shared_widget.dart';
|
||||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/sidebar_space_header.dart';
|
import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/sidebar_space_header.dart';
|
||||||
import 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart';
|
|
||||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
|
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
|
||||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
@ -115,11 +111,19 @@ class _SpaceState extends State<_Space> {
|
|||||||
MouseRegion(
|
MouseRegion(
|
||||||
onEnter: (_) => isHovered.value = true,
|
onEnter: (_) => isHovered.value = true,
|
||||||
onExit: (_) => isHovered.value = false,
|
onExit: (_) => isHovered.value = false,
|
||||||
child: _Pages(
|
child: SpacePages(
|
||||||
key: ValueKey(currentSpace.id),
|
key: ValueKey(currentSpace.id),
|
||||||
isExpandedNotifier: isExpandedNotifier,
|
isExpandedNotifier: isExpandedNotifier,
|
||||||
space: currentSpace,
|
space: currentSpace,
|
||||||
isHovered: isHovered,
|
isHovered: isHovered,
|
||||||
|
onSelected: (context, view) {
|
||||||
|
if (HardwareKeyboard.instance.isControlPressed) {
|
||||||
|
context.read<TabsBloc>().openTab(view);
|
||||||
|
}
|
||||||
|
context.read<TabsBloc>().openPlugin(view);
|
||||||
|
},
|
||||||
|
onTertiarySelected: (context, view) =>
|
||||||
|
context.read<TabsBloc>().openTab(view),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@ -169,57 +173,3 @@ class _SpaceState extends State<_Space> {
|
|||||||
context.read<SpaceBloc>().add(const SpaceEvent.switchToNextSpace());
|
context.read<SpaceBloc>().add(const SpaceEvent.switchToNextSpace());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _Pages extends StatelessWidget {
|
|
||||||
const _Pages({
|
|
||||||
super.key,
|
|
||||||
required this.space,
|
|
||||||
required this.isHovered,
|
|
||||||
required this.isExpandedNotifier,
|
|
||||||
});
|
|
||||||
|
|
||||||
final ViewPB space;
|
|
||||||
final ValueNotifier<bool> isHovered;
|
|
||||||
final PropertyValueNotifier<bool> isExpandedNotifier;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return BlocProvider(
|
|
||||||
create: (context) =>
|
|
||||||
ViewBloc(view: space)..add(const ViewEvent.initial()),
|
|
||||||
child: BlocBuilder<ViewBloc, ViewState>(
|
|
||||||
builder: (context, state) {
|
|
||||||
return Column(
|
|
||||||
children: state.view.childViews
|
|
||||||
.map(
|
|
||||||
(view) => ViewItem(
|
|
||||||
key: ValueKey('${space.id} ${view.id}'),
|
|
||||||
spaceType:
|
|
||||||
space.spacePermission == SpacePermission.publicToAll
|
|
||||||
? FolderSpaceType.public
|
|
||||||
: FolderSpaceType.private,
|
|
||||||
isFirstChild: view.id == state.view.childViews.first.id,
|
|
||||||
view: view,
|
|
||||||
level: 0,
|
|
||||||
leftPadding: HomeSpaceViewSizes.leftPadding,
|
|
||||||
isFeedback: false,
|
|
||||||
isHovered: isHovered,
|
|
||||||
isExpandedNotifier: isExpandedNotifier,
|
|
||||||
onSelected: (viewContext, view) {
|
|
||||||
if (HardwareKeyboard.instance.isControlPressed) {
|
|
||||||
context.read<TabsBloc>().openTab(view);
|
|
||||||
}
|
|
||||||
|
|
||||||
context.read<TabsBloc>().openPlugin(view);
|
|
||||||
},
|
|
||||||
onTertiarySelected: (viewContext, view) =>
|
|
||||||
context.read<TabsBloc>().openTab(view),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.toList(),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -7,13 +7,10 @@ import 'package:appflowy/workspace/application/sidebar/space/space_bloc.dart';
|
|||||||
import 'package:appflowy/workspace/presentation/home/home_sizes.dart';
|
import 'package:appflowy/workspace/presentation/home/home_sizes.dart';
|
||||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/manage_space_popup.dart';
|
import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/manage_space_popup.dart';
|
||||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/shared_widget.dart';
|
import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/shared_widget.dart';
|
||||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/sidebar_space_menu.dart';
|
|
||||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/space_action_type.dart';
|
import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/space_action_type.dart';
|
||||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/space_icon.dart';
|
|
||||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/space_more_popup.dart';
|
import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/space_more_popup.dart';
|
||||||
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
|
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.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:easy_localization/easy_localization.dart';
|
||||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||||
import 'package:flowy_infra_ui/style_widget/hover.dart';
|
import 'package:flowy_infra_ui/style_widget/hover.dart';
|
||||||
@ -68,24 +65,8 @@ class _SidebarSpaceHeaderState extends State<SidebarSpaceHeader> {
|
|||||||
left: 3,
|
left: 3,
|
||||||
top: 3,
|
top: 3,
|
||||||
bottom: 3,
|
bottom: 3,
|
||||||
child: SizedBox(
|
child: SpacePopup(
|
||||||
height: HomeSizes.workspaceSectionHeight,
|
child: _buildChild(),
|
||||||
child: AppFlowyPopover(
|
|
||||||
constraints: const BoxConstraints(maxWidth: 252),
|
|
||||||
direction: PopoverDirection.bottomWithLeftAligned,
|
|
||||||
clickHandler: PopoverClickHandler.gestureDetector,
|
|
||||||
offset: const Offset(0, 4),
|
|
||||||
popupBuilder: (_) => BlocProvider.value(
|
|
||||||
value: context.read<SpaceBloc>(),
|
|
||||||
child: const SidebarSpaceMenu(),
|
|
||||||
),
|
|
||||||
child: FlowyButton(
|
|
||||||
useIntrinsicWidth: true,
|
|
||||||
margin: const EdgeInsets.only(left: 3.0, right: 4.0),
|
|
||||||
iconPadding: 10.0,
|
|
||||||
text: _buildChild(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Positioned(
|
Positioned(
|
||||||
@ -135,29 +116,8 @@ class _SidebarSpaceHeaderState extends State<SidebarSpaceHeader> {
|
|||||||
);
|
);
|
||||||
return FlowyTooltip(
|
return FlowyTooltip(
|
||||||
richMessage: textSpan,
|
richMessage: textSpan,
|
||||||
child: Row(
|
child: CurrentSpace(
|
||||||
children: [
|
space: widget.space,
|
||||||
SpaceIcon(
|
|
||||||
dimension: 20,
|
|
||||||
space: widget.space,
|
|
||||||
cornerRadius: 6.0,
|
|
||||||
),
|
|
||||||
const HSpace(10),
|
|
||||||
Flexible(
|
|
||||||
child: FlowyText.medium(
|
|
||||||
widget.space.name,
|
|
||||||
lineHeight: 1.15,
|
|
||||||
fontSize: 14.0,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const HSpace(4.0),
|
|
||||||
FlowySvg(
|
|
||||||
widget.isExpanded
|
|
||||||
? FlowySvgs.workspace_drop_down_menu_show_s
|
|
||||||
: FlowySvgs.workspace_drop_down_menu_hide_s,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -52,7 +52,7 @@ class ViewItem extends StatelessWidget {
|
|||||||
this.isDraggable = true,
|
this.isDraggable = true,
|
||||||
required this.isFeedback,
|
required this.isFeedback,
|
||||||
this.height = HomeSpaceViewSizes.viewHeight,
|
this.height = HomeSpaceViewSizes.viewHeight,
|
||||||
this.isHoverEnabled = true,
|
this.isHoverEnabled = false,
|
||||||
this.isPlaceholder = false,
|
this.isPlaceholder = false,
|
||||||
this.isHovered,
|
this.isHovered,
|
||||||
this.shouldRenderChildren = true,
|
this.shouldRenderChildren = true,
|
||||||
@ -61,6 +61,8 @@ class ViewItem extends StatelessWidget {
|
|||||||
this.shouldLoadChildViews = true,
|
this.shouldLoadChildViews = true,
|
||||||
this.isExpandedNotifier,
|
this.isExpandedNotifier,
|
||||||
this.extendBuilder,
|
this.extendBuilder,
|
||||||
|
this.disableSelectedStatus,
|
||||||
|
this.shouldIgnoreView,
|
||||||
});
|
});
|
||||||
|
|
||||||
final ViewPB view;
|
final ViewPB view;
|
||||||
@ -116,6 +118,12 @@ class ViewItem extends StatelessWidget {
|
|||||||
|
|
||||||
final List<Widget> Function(ViewPB view)? extendBuilder;
|
final List<Widget> Function(ViewPB view)? extendBuilder;
|
||||||
|
|
||||||
|
// disable the selected status of the view item
|
||||||
|
final bool? disableSelectedStatus;
|
||||||
|
|
||||||
|
// ignore the views when rendering the child views
|
||||||
|
final bool Function(ViewPB view)? shouldIgnoreView;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return BlocProvider(
|
return BlocProvider(
|
||||||
@ -129,15 +137,23 @@ class ViewItem extends StatelessWidget {
|
|||||||
listener: (context, state) =>
|
listener: (context, state) =>
|
||||||
context.read<TabsBloc>().openPlugin(state.lastCreatedView!),
|
context.read<TabsBloc>().openPlugin(state.lastCreatedView!),
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
|
// filter the child views that should be ignored
|
||||||
|
var childViews = state.view.childViews;
|
||||||
|
if (shouldIgnoreView != null) {
|
||||||
|
childViews = childViews
|
||||||
|
.where((childView) => !shouldIgnoreView!(childView))
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
return InnerViewItem(
|
return InnerViewItem(
|
||||||
view: state.view,
|
view: state.view,
|
||||||
parentView: parentView,
|
parentView: parentView,
|
||||||
childViews: state.view.childViews,
|
childViews: childViews,
|
||||||
spaceType: spaceType,
|
spaceType: spaceType,
|
||||||
level: level,
|
level: level,
|
||||||
leftPadding: leftPadding,
|
leftPadding: leftPadding,
|
||||||
showActions: state.isEditing,
|
showActions: state.isEditing,
|
||||||
isExpanded: state.isExpanded,
|
isExpanded: state.isExpanded,
|
||||||
|
disableSelectedStatus: disableSelectedStatus,
|
||||||
onSelected: onSelected,
|
onSelected: onSelected,
|
||||||
onTertiarySelected: onTertiarySelected,
|
onTertiarySelected: onTertiarySelected,
|
||||||
isFirstChild: isFirstChild,
|
isFirstChild: isFirstChild,
|
||||||
@ -152,6 +168,7 @@ class ViewItem extends StatelessWidget {
|
|||||||
rightIconsBuilder: rightIconsBuilder,
|
rightIconsBuilder: rightIconsBuilder,
|
||||||
isExpandedNotifier: isExpandedNotifier,
|
isExpandedNotifier: isExpandedNotifier,
|
||||||
extendBuilder: extendBuilder,
|
extendBuilder: extendBuilder,
|
||||||
|
shouldIgnoreView: shouldIgnoreView,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@ -186,6 +203,8 @@ class InnerViewItem extends StatefulWidget {
|
|||||||
required this.rightIconsBuilder,
|
required this.rightIconsBuilder,
|
||||||
this.isExpandedNotifier,
|
this.isExpandedNotifier,
|
||||||
required this.extendBuilder,
|
required this.extendBuilder,
|
||||||
|
this.disableSelectedStatus,
|
||||||
|
required this.shouldIgnoreView,
|
||||||
});
|
});
|
||||||
|
|
||||||
final ViewPB view;
|
final ViewPB view;
|
||||||
@ -209,6 +228,7 @@ class InnerViewItem extends StatefulWidget {
|
|||||||
|
|
||||||
final bool isHoverEnabled;
|
final bool isHoverEnabled;
|
||||||
final bool isPlaceholder;
|
final bool isPlaceholder;
|
||||||
|
final bool? disableSelectedStatus;
|
||||||
final ValueNotifier<bool>? isHovered;
|
final ValueNotifier<bool>? isHovered;
|
||||||
final bool shouldRenderChildren;
|
final bool shouldRenderChildren;
|
||||||
final ViewItemLeftIconBuilder? leftIconBuilder;
|
final ViewItemLeftIconBuilder? leftIconBuilder;
|
||||||
@ -216,6 +236,7 @@ class InnerViewItem extends StatefulWidget {
|
|||||||
|
|
||||||
final PropertyValueNotifier<bool>? isExpandedNotifier;
|
final PropertyValueNotifier<bool>? isExpandedNotifier;
|
||||||
final List<Widget> Function(ViewPB view)? extendBuilder;
|
final List<Widget> Function(ViewPB view)? extendBuilder;
|
||||||
|
final bool Function(ViewPB view)? shouldIgnoreView;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<InnerViewItem> createState() => _InnerViewItemState();
|
State<InnerViewItem> createState() => _InnerViewItemState();
|
||||||
@ -254,6 +275,8 @@ class _InnerViewItemState extends State<InnerViewItem> {
|
|||||||
leftIconBuilder: widget.leftIconBuilder,
|
leftIconBuilder: widget.leftIconBuilder,
|
||||||
rightIconsBuilder: widget.rightIconsBuilder,
|
rightIconsBuilder: widget.rightIconsBuilder,
|
||||||
extendBuilder: widget.extendBuilder,
|
extendBuilder: widget.extendBuilder,
|
||||||
|
disableSelectedStatus: widget.disableSelectedStatus,
|
||||||
|
shouldIgnoreView: widget.shouldIgnoreView,
|
||||||
);
|
);
|
||||||
|
|
||||||
// if the view is expanded and has child views, render its child views
|
// if the view is expanded and has child views, render its child views
|
||||||
@ -271,6 +294,7 @@ class _InnerViewItemState extends State<InnerViewItem> {
|
|||||||
onSelected: widget.onSelected,
|
onSelected: widget.onSelected,
|
||||||
onTertiarySelected: widget.onTertiarySelected,
|
onTertiarySelected: widget.onTertiarySelected,
|
||||||
isDraggable: widget.isDraggable,
|
isDraggable: widget.isDraggable,
|
||||||
|
disableSelectedStatus: widget.disableSelectedStatus,
|
||||||
leftPadding: widget.leftPadding,
|
leftPadding: widget.leftPadding,
|
||||||
isFeedback: widget.isFeedback,
|
isFeedback: widget.isFeedback,
|
||||||
isPlaceholder: widget.isPlaceholder,
|
isPlaceholder: widget.isPlaceholder,
|
||||||
@ -278,6 +302,7 @@ class _InnerViewItemState extends State<InnerViewItem> {
|
|||||||
leftIconBuilder: widget.leftIconBuilder,
|
leftIconBuilder: widget.leftIconBuilder,
|
||||||
rightIconsBuilder: widget.rightIconsBuilder,
|
rightIconsBuilder: widget.rightIconsBuilder,
|
||||||
extendBuilder: widget.extendBuilder,
|
extendBuilder: widget.extendBuilder,
|
||||||
|
shouldIgnoreView: widget.shouldIgnoreView,
|
||||||
);
|
);
|
||||||
}).toList();
|
}).toList();
|
||||||
|
|
||||||
@ -300,7 +325,14 @@ class _InnerViewItemState extends State<InnerViewItem> {
|
|||||||
_isDragging = isDragging;
|
_isDragging = isDragging;
|
||||||
},
|
},
|
||||||
onMove: widget.isPlaceholder
|
onMove: widget.isPlaceholder
|
||||||
? (from, to) => _moveViewCrossSection(context, from, to)
|
? (from, to) => _moveViewCrossSection(
|
||||||
|
context,
|
||||||
|
widget.view,
|
||||||
|
widget.parentView,
|
||||||
|
widget.spaceType,
|
||||||
|
from,
|
||||||
|
to.parentViewId,
|
||||||
|
)
|
||||||
: null,
|
: null,
|
||||||
feedback: (context) {
|
feedback: (context) {
|
||||||
return Container(
|
return Container(
|
||||||
@ -324,6 +356,7 @@ class _InnerViewItemState extends State<InnerViewItem> {
|
|||||||
leftIconBuilder: widget.leftIconBuilder,
|
leftIconBuilder: widget.leftIconBuilder,
|
||||||
rightIconsBuilder: widget.rightIconsBuilder,
|
rightIconsBuilder: widget.rightIconsBuilder,
|
||||||
extendBuilder: widget.extendBuilder,
|
extendBuilder: widget.extendBuilder,
|
||||||
|
shouldIgnoreView: widget.shouldIgnoreView,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@ -345,37 +378,6 @@ class _InnerViewItemState extends State<InnerViewItem> {
|
|||||||
context.read<ViewBloc>().add(const ViewEvent.collapseAllPages());
|
context.read<ViewBloc>().add(const ViewEvent.collapseAllPages());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _moveViewCrossSection(
|
|
||||||
BuildContext context,
|
|
||||||
ViewPB from,
|
|
||||||
ViewPB to,
|
|
||||||
) {
|
|
||||||
if (isReferencedDatabaseView(widget.view, widget.parentView)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
final fromSection = widget.spaceType == FolderSpaceType.public
|
|
||||||
? ViewSectionPB.Private
|
|
||||||
: ViewSectionPB.Public;
|
|
||||||
final toSection = widget.spaceType == FolderSpaceType.public
|
|
||||||
? ViewSectionPB.Public
|
|
||||||
: ViewSectionPB.Private;
|
|
||||||
context.read<ViewBloc>().add(
|
|
||||||
ViewEvent.move(
|
|
||||||
from,
|
|
||||||
to.parentViewId,
|
|
||||||
null,
|
|
||||||
fromSection,
|
|
||||||
toSection,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
context.read<ViewBloc>().add(
|
|
||||||
ViewEvent.updateViewVisibility(
|
|
||||||
from,
|
|
||||||
widget.spaceType == FolderSpaceType.public,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class SingleInnerViewItem extends StatefulWidget {
|
class SingleInnerViewItem extends StatefulWidget {
|
||||||
@ -399,6 +401,8 @@ class SingleInnerViewItem extends StatefulWidget {
|
|||||||
required this.leftIconBuilder,
|
required this.leftIconBuilder,
|
||||||
required this.rightIconsBuilder,
|
required this.rightIconsBuilder,
|
||||||
required this.extendBuilder,
|
required this.extendBuilder,
|
||||||
|
required this.disableSelectedStatus,
|
||||||
|
required this.shouldIgnoreView,
|
||||||
});
|
});
|
||||||
|
|
||||||
final ViewPB view;
|
final ViewPB view;
|
||||||
@ -419,11 +423,13 @@ class SingleInnerViewItem extends StatefulWidget {
|
|||||||
|
|
||||||
final bool isHoverEnabled;
|
final bool isHoverEnabled;
|
||||||
final bool isPlaceholder;
|
final bool isPlaceholder;
|
||||||
|
final bool? disableSelectedStatus;
|
||||||
final ValueNotifier<bool>? isHovered;
|
final ValueNotifier<bool>? isHovered;
|
||||||
final ViewItemLeftIconBuilder? leftIconBuilder;
|
final ViewItemLeftIconBuilder? leftIconBuilder;
|
||||||
final ViewItemRightIconsBuilder? rightIconsBuilder;
|
final ViewItemRightIconsBuilder? rightIconsBuilder;
|
||||||
|
|
||||||
final List<Widget> Function(ViewPB view)? extendBuilder;
|
final List<Widget> Function(ViewPB view)? extendBuilder;
|
||||||
|
final bool Function(ViewPB view)? shouldIgnoreView;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<SingleInnerViewItem> createState() => _SingleInnerViewItemState();
|
State<SingleInnerViewItem> createState() => _SingleInnerViewItemState();
|
||||||
@ -435,8 +441,11 @@ class _SingleInnerViewItemState extends State<SingleInnerViewItem> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final isSelected =
|
var isSelected =
|
||||||
getIt<MenuSharedState>().latestOpenView?.id == widget.view.id;
|
getIt<MenuSharedState>().latestOpenView?.id == widget.view.id;
|
||||||
|
if (widget.disableSelectedStatus == true) {
|
||||||
|
isSelected = false;
|
||||||
|
}
|
||||||
|
|
||||||
if (widget.isPlaceholder) {
|
if (widget.isPlaceholder) {
|
||||||
return const SizedBox(
|
return const SizedBox(
|
||||||
@ -714,6 +723,22 @@ class _SingleInnerViewItemState extends State<SingleInnerViewItem> {
|
|||||||
iconType: result.type.toProto(),
|
iconType: result.type.toProto(),
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
|
case ViewMoreActionType.moveTo:
|
||||||
|
final target = data;
|
||||||
|
if (target is! ViewPB) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
debugPrint(
|
||||||
|
'Move view ${widget.view.id}, ${widget.view.name} to ${target.id}, ${target.name}',
|
||||||
|
);
|
||||||
|
_moveViewCrossSection(
|
||||||
|
context,
|
||||||
|
widget.view,
|
||||||
|
widget.parentView,
|
||||||
|
widget.spaceType,
|
||||||
|
widget.view,
|
||||||
|
target.id,
|
||||||
|
);
|
||||||
default:
|
default:
|
||||||
throw UnsupportedError('$action is not supported');
|
throw UnsupportedError('$action is not supported');
|
||||||
}
|
}
|
||||||
@ -765,3 +790,42 @@ bool isReferencedDatabaseView(ViewPB view, ViewPB? parentView) {
|
|||||||
}
|
}
|
||||||
return view.layout.isDatabaseView && parentView.layout.isDatabaseView;
|
return view.layout.isDatabaseView && parentView.layout.isDatabaseView;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _moveViewCrossSection(
|
||||||
|
BuildContext context,
|
||||||
|
ViewPB view,
|
||||||
|
ViewPB? parentView,
|
||||||
|
FolderSpaceType spaceType,
|
||||||
|
ViewPB from,
|
||||||
|
String toId,
|
||||||
|
) {
|
||||||
|
if (isReferencedDatabaseView(view, parentView)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (from.id == toId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final fromSection = spaceType == FolderSpaceType.public
|
||||||
|
? ViewSectionPB.Private
|
||||||
|
: ViewSectionPB.Public;
|
||||||
|
final toSection = spaceType == FolderSpaceType.public
|
||||||
|
? ViewSectionPB.Public
|
||||||
|
: ViewSectionPB.Private;
|
||||||
|
context.read<ViewBloc>().add(
|
||||||
|
ViewEvent.move(
|
||||||
|
from,
|
||||||
|
toId,
|
||||||
|
null,
|
||||||
|
fromSection,
|
||||||
|
toSection,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
context.read<ViewBloc>().add(
|
||||||
|
ViewEvent.updateViewVisibility(
|
||||||
|
from,
|
||||||
|
spaceType == FolderSpaceType.public,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
@ -1,12 +1,15 @@
|
|||||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||||
import 'package:appflowy/plugins/base/icon/icon_picker.dart';
|
import 'package:appflowy/plugins/base/icon/icon_picker.dart';
|
||||||
import 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart';
|
import 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart';
|
||||||
|
import 'package:appflowy/workspace/application/sidebar/space/space_bloc.dart';
|
||||||
|
import 'package:appflowy/workspace/presentation/home/menu/sidebar/move_to/move_page_menu.dart';
|
||||||
import 'package:appflowy/workspace/presentation/home/menu/view/view_action_type.dart';
|
import 'package:appflowy/workspace/presentation/home/menu/view/view_action_type.dart';
|
||||||
import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart';
|
import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
|
||||||
/// ··· button beside the view name
|
/// ··· button beside the view name
|
||||||
class ViewMoreActionButton extends StatelessWidget {
|
class ViewMoreActionButton extends StatelessWidget {
|
||||||
@ -52,7 +55,7 @@ class ViewMoreActionButton extends StatelessWidget {
|
|||||||
final actionTypes = _buildActionTypes();
|
final actionTypes = _buildActionTypes();
|
||||||
return actionTypes
|
return actionTypes
|
||||||
.map(
|
.map(
|
||||||
(e) => ViewMoreActionTypeWrapper(e, (controller, data) {
|
(e) => ViewMoreActionTypeWrapper(e, view, (controller, data) {
|
||||||
onEditing(false);
|
onEditing(false);
|
||||||
onAction(e, data);
|
onAction(e, data);
|
||||||
controller.close();
|
controller.close();
|
||||||
@ -92,6 +95,7 @@ class ViewMoreActionButton extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
actionTypes.addAll([
|
actionTypes.addAll([
|
||||||
|
ViewMoreActionType.moveTo,
|
||||||
ViewMoreActionType.delete,
|
ViewMoreActionType.delete,
|
||||||
ViewMoreActionType.divider,
|
ViewMoreActionType.divider,
|
||||||
]);
|
]);
|
||||||
@ -110,9 +114,14 @@ class ViewMoreActionButton extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class ViewMoreActionTypeWrapper extends CustomActionCell {
|
class ViewMoreActionTypeWrapper extends CustomActionCell {
|
||||||
ViewMoreActionTypeWrapper(this.inner, this.onTap);
|
ViewMoreActionTypeWrapper(
|
||||||
|
this.inner,
|
||||||
|
this.sourceView,
|
||||||
|
this.onTap,
|
||||||
|
);
|
||||||
|
|
||||||
final ViewMoreActionType inner;
|
final ViewMoreActionType inner;
|
||||||
|
final ViewPB sourceView;
|
||||||
final void Function(PopoverController controller, dynamic data) onTap;
|
final void Function(PopoverController controller, dynamic data) onTap;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -125,9 +134,11 @@ class ViewMoreActionTypeWrapper extends CustomActionCell {
|
|||||||
return _buildCreated(context);
|
return _buildCreated(context);
|
||||||
} else if (inner == ViewMoreActionType.changeIcon) {
|
} else if (inner == ViewMoreActionType.changeIcon) {
|
||||||
return _buildEmojiActionButton(context, controller);
|
return _buildEmojiActionButton(context, controller);
|
||||||
} else {
|
} else if (inner == ViewMoreActionType.moveTo) {
|
||||||
return _buildNormalActionButton(context, controller);
|
return _buildMoveToActionButton(context, controller);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return _buildNormalActionButton(context, controller);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildNormalActionButton(
|
Widget _buildNormalActionButton(
|
||||||
@ -154,6 +165,42 @@ class ViewMoreActionTypeWrapper extends CustomActionCell {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _buildMoveToActionButton(
|
||||||
|
BuildContext context,
|
||||||
|
PopoverController controller,
|
||||||
|
) {
|
||||||
|
final child = _buildActionButton(context, null);
|
||||||
|
final userProfile = context.read<SpaceBloc>().userProfile;
|
||||||
|
final workspaceId = context.read<SpaceBloc>().state.currentSpace?.id;
|
||||||
|
|
||||||
|
return AppFlowyPopover(
|
||||||
|
constraints: const BoxConstraints(
|
||||||
|
maxWidth: 260,
|
||||||
|
maxHeight: 345,
|
||||||
|
),
|
||||||
|
margin: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 14.0,
|
||||||
|
vertical: 12.0,
|
||||||
|
),
|
||||||
|
clickHandler: PopoverClickHandler.gestureDetector,
|
||||||
|
popupBuilder: (_) {
|
||||||
|
if (workspaceId == null) {
|
||||||
|
return const SizedBox();
|
||||||
|
}
|
||||||
|
|
||||||
|
return MovePageMenu(
|
||||||
|
sourceView: sourceView,
|
||||||
|
userProfile: userProfile,
|
||||||
|
workspaceId: workspaceId,
|
||||||
|
onSelected: (view) {
|
||||||
|
onTap(controller, view);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: child,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Widget _buildDivider() {
|
Widget _buildDivider() {
|
||||||
return const Padding(
|
return const Padding(
|
||||||
padding: EdgeInsets.all(8.0),
|
padding: EdgeInsets.all(8.0),
|
||||||
|
6
frontend/resources/flowy_icons/16x/magnifier.svg
Normal file
6
frontend/resources/flowy_icons/16x/magnifier.svg
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<g opacity="0.6">
|
||||||
|
<path d="M7.66536 14.0007C11.1632 14.0007 13.9987 11.1651 13.9987 7.66732C13.9987 4.16951 11.1632 1.33398 7.66536 1.33398C4.16756 1.33398 1.33203 4.16951 1.33203 7.66732C1.33203 11.1651 4.16756 14.0007 7.66536 14.0007Z" stroke="#171717" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M14.6654 14.6673L13.332 13.334" stroke="#171717" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 519 B |
Reference in New Issue
Block a user