feat: support moving view across sections (#5015)

This commit is contained in:
Lucas.Xu 2024-04-01 14:27:29 +08:00 committed by GitHub
parent 893d23d6a3
commit 723423d423
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 140 additions and 2 deletions

View File

@ -207,6 +207,13 @@ class ViewBloc extends Bloc<ViewEvent, ViewState> {
), ),
); );
}, },
updateViewVisibility: (value) async {
final view = value.view;
await ViewBackendService.updateViewsVisibility(
[view],
value.isPublic,
);
},
); );
}, },
); );
@ -370,6 +377,8 @@ class ViewEvent with _$ViewEvent {
) = ViewDidUpdate; ) = ViewDidUpdate;
const factory ViewEvent.viewUpdateChildView(ViewPB result) = const factory ViewEvent.viewUpdateChildView(ViewPB result) =
ViewUpdateChildView; ViewUpdateChildView;
const factory ViewEvent.updateViewVisibility(ViewPB view, bool isPublic) =
UpdateViewVisibility;
} }
@freezed @freezed

View File

@ -280,4 +280,15 @@ class ViewBackendService {
); );
}); });
} }
static Future<FlowyResult<void, FlowyError>> updateViewsVisibility(
List<ViewPB> views,
bool isPublic,
) async {
final payload = UpdateViewVisibilityStatusPayloadPB(
viewIds: views.map((e) => e.id).toList(),
isPublic: isPublic,
);
return FolderEventUpdateViewVisibilityStatus(payload).send();
}
} }

View File

@ -2,6 +2,7 @@ import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/workspace/application/menu/sidebar_sections_bloc.dart'; import 'package:appflowy/workspace/application/menu/sidebar_sections_bloc.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/tabs/tabs_bloc.dart'; import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart';
import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/folder/_folder_header.dart'; import 'package:appflowy/workspace/presentation/home/menu/sidebar/folder/_folder_header.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/rename_view_dialog.dart'; import 'package:appflowy/workspace/presentation/home/menu/sidebar/rename_view_dialog.dart';
import 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart'; import 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart';
@ -95,6 +96,25 @@ class SectionFolder extends StatelessWidget {
isHoverEnabled: isHoverEnabled, isHoverEnabled: isHoverEnabled,
), ),
), ),
if (views.isEmpty)
ViewItem(
categoryType: categoryType,
view: ViewPB(
parentViewId: context
.read<UserWorkspaceBloc>()
.state
.currentWorkspace
?.workspaceId ??
'',
),
level: 0,
leftPadding: 16,
isFeedback: false,
onSelected: (_) {},
onTertiarySelected: (_) {},
isHoverEnabled: isHoverEnabled,
isPlaceholder: true,
),
], ],
); );
}, },

View File

@ -26,6 +26,7 @@ class DraggableViewItem extends StatefulWidget {
this.topHighlightColor, this.topHighlightColor,
this.bottomHighlightColor, this.bottomHighlightColor,
this.onDragging, this.onDragging,
this.onMove,
}); });
final Widget child; final Widget child;
@ -36,6 +37,7 @@ class DraggableViewItem extends StatefulWidget {
final Color? topHighlightColor; final Color? topHighlightColor;
final Color? bottomHighlightColor; final Color? bottomHighlightColor;
final void Function(bool isDragging)? onDragging; final void Function(bool isDragging)? onDragging;
final void Function(ViewPB from, ViewPB to)? onMove;
@override @override
State<DraggableViewItem> createState() => _DraggableViewItemState(); State<DraggableViewItem> createState() => _DraggableViewItemState();
@ -189,6 +191,11 @@ class _DraggableViewItemState extends State<DraggableViewItem> {
return; return;
} }
if (widget.onMove != null) {
widget.onMove?.call(from, to);
return;
}
final fromSection = getViewSection(from); final fromSection = getViewSection(from);
final toSection = getViewSection(to); final toSection = getViewSection(to);

View File

@ -43,6 +43,7 @@ class ViewItem extends StatelessWidget {
required this.isFeedback, required this.isFeedback,
this.height = 28.0, this.height = 28.0,
this.isHoverEnabled = true, this.isHoverEnabled = true,
this.isPlaceholder = false,
}); });
final ViewPB view; final ViewPB view;
@ -78,6 +79,10 @@ class ViewItem extends StatelessWidget {
final bool isHoverEnabled; final bool isHoverEnabled;
// all the view movement depends on the [ViewItem] widget, so we have to add a
// placeholder widget to receive the drop event when moving view across sections.
final bool isPlaceholder;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return BlocProvider( return BlocProvider(
@ -105,6 +110,7 @@ class ViewItem extends StatelessWidget {
isFeedback: isFeedback, isFeedback: isFeedback,
height: height, height: height,
isHoverEnabled: isHoverEnabled, isHoverEnabled: isHoverEnabled,
isPlaceholder: isPlaceholder,
); );
}, },
), ),
@ -132,6 +138,7 @@ class InnerViewItem extends StatelessWidget {
required this.isFeedback, required this.isFeedback,
required this.height, required this.height,
this.isHoverEnabled = true, this.isHoverEnabled = true,
this.isPlaceholder = false,
}); });
final ViewPB view; final ViewPB view;
@ -154,6 +161,7 @@ class InnerViewItem extends StatelessWidget {
final double height; final double height;
final bool isHoverEnabled; final bool isHoverEnabled;
final bool isPlaceholder;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -170,6 +178,7 @@ class InnerViewItem extends StatelessWidget {
leftPadding: leftPadding, leftPadding: leftPadding,
isFeedback: isFeedback, isFeedback: isFeedback,
height: height, height: height,
isPlaceholder: isPlaceholder,
); );
// 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
@ -188,6 +197,7 @@ class InnerViewItem extends StatelessWidget {
isDraggable: isDraggable, isDraggable: isDraggable,
leftPadding: leftPadding, leftPadding: leftPadding,
isFeedback: isFeedback, isFeedback: isFeedback,
isPlaceholder: isPlaceholder,
); );
}).toList(); }).toList();
@ -222,14 +232,17 @@ class InnerViewItem extends StatelessWidget {
} }
// wrap the child with DraggableItem if isDraggable is true // wrap the child with DraggableItem if isDraggable is true
if (isDraggable && !isReferencedDatabaseView(view, parentView)) { if ((isDraggable || isPlaceholder) &&
!isReferencedDatabaseView(view, parentView)) {
child = DraggableViewItem( child = DraggableViewItem(
isFirstChild: isFirstChild, isFirstChild: isFirstChild,
view: view, view: view,
child: child,
onDragging: (isDragging) { onDragging: (isDragging) {
_isDragging = isDragging; _isDragging = isDragging;
}, },
onMove: isPlaceholder
? (from, to) => _moveViewCrossSection(context, from, to)
: null,
feedback: (context) { feedback: (context) {
return ViewItem( return ViewItem(
view: view, view: view,
@ -243,6 +256,7 @@ class InnerViewItem extends StatelessWidget {
isFeedback: true, isFeedback: true,
); );
}, },
child: child,
); );
} else { } else {
// keep the same height of the DraggableItem // keep the same height of the DraggableItem
@ -254,6 +268,37 @@ class InnerViewItem extends StatelessWidget {
return child; return child;
} }
void _moveViewCrossSection(
BuildContext context,
ViewPB from,
ViewPB to,
) {
if (isReferencedDatabaseView(view, parentView)) {
return;
}
final fromSection = categoryType == FolderCategoryType.public
? ViewSectionPB.Private
: ViewSectionPB.Public;
final toSection = categoryType == FolderCategoryType.public
? ViewSectionPB.Public
: ViewSectionPB.Private;
context.read<ViewBloc>().add(
ViewEvent.move(
from,
to.parentViewId,
null,
fromSection,
toSection,
),
);
context.read<ViewBloc>().add(
ViewEvent.updateViewVisibility(
from,
categoryType == FolderCategoryType.public,
),
);
}
} }
class SingleInnerViewItem extends StatefulWidget { class SingleInnerViewItem extends StatefulWidget {
@ -272,6 +317,7 @@ class SingleInnerViewItem extends StatefulWidget {
required this.isFeedback, required this.isFeedback,
required this.height, required this.height,
this.isHoverEnabled = true, this.isHoverEnabled = true,
this.isPlaceholder = false,
}); });
final ViewPB view; final ViewPB view;
@ -291,6 +337,7 @@ class SingleInnerViewItem extends StatefulWidget {
final double height; final double height;
final bool isHoverEnabled; final bool isHoverEnabled;
final bool isPlaceholder;
@override @override
State<SingleInnerViewItem> createState() => _SingleInnerViewItemState(); State<SingleInnerViewItem> createState() => _SingleInnerViewItemState();
@ -305,6 +352,13 @@ class _SingleInnerViewItemState extends State<SingleInnerViewItem> {
final isSelected = final isSelected =
getIt<MenuSharedState>().latestOpenView?.id == widget.view.id; getIt<MenuSharedState>().latestOpenView?.id == widget.view.id;
if (widget.isPlaceholder) {
return const SizedBox(
height: 4,
width: double.infinity,
);
}
if (widget.isFeedback || !widget.isHoverEnabled) { if (widget.isFeedback || !widget.isHoverEnabled) {
return _buildViewItem( return _buildViewItem(
false, false,

View File

@ -475,6 +475,15 @@ pub struct UpdateRecentViewPayloadPB {
pub add_in_recent: bool, pub add_in_recent: bool,
} }
#[derive(Default, ProtoBuf)]
pub struct UpdateViewVisibilityStatusPayloadPB {
#[pb(index = 1)]
pub view_ids: Vec<String>,
#[pb(index = 2)]
pub is_public: bool,
}
// impl<'de> Deserialize<'de> for ViewDataType { // impl<'de> Deserialize<'de> for ViewDataType {
// fn deserialize<D>(deserializer: D) -> Result<Self, <D as Deserializer<'de>>::Error> // fn deserialize<D>(deserializer: D) -> Result<Self, <D as Deserializer<'de>>::Error>
// where // where

View File

@ -363,3 +363,14 @@ pub(crate) async fn reload_workspace_handler(
folder.reload_workspace().await?; folder.reload_workspace().await?;
Ok(()) Ok(())
} }
#[tracing::instrument(level = "debug", skip(data, folder), err)]
pub(crate) async fn update_view_visibility_status_handler(
data: AFPluginData<UpdateViewVisibilityStatusPayloadPB>,
folder: AFPluginState<Weak<FolderManager>>,
) -> Result<(), FlowyError> {
let folder = upgrade_folder(folder)?;
let params = data.into_inner();
folder.set_views_visibility(params.view_ids, params.is_public);
Ok(())
}

View File

@ -40,6 +40,7 @@ pub fn init(folder: Weak<FolderManager>) -> AFPlugin {
.event(FolderEvent::ReloadWorkspace, reload_workspace_handler) .event(FolderEvent::ReloadWorkspace, reload_workspace_handler)
.event(FolderEvent::ReadPrivateViews, read_private_views_handler) .event(FolderEvent::ReadPrivateViews, read_private_views_handler)
.event(FolderEvent::ReadCurrentWorkspaceViews, get_current_workspace_views_handler) .event(FolderEvent::ReadCurrentWorkspaceViews, get_current_workspace_views_handler)
.event(FolderEvent::UpdateViewVisibilityStatus, update_view_visibility_status_handler)
} }
#[derive(Clone, Copy, PartialEq, Eq, Debug, Display, Hash, ProtoBuf_Enum, Flowy_Event)] #[derive(Clone, Copy, PartialEq, Eq, Debug, Display, Hash, ProtoBuf_Enum, Flowy_Event)]
@ -166,4 +167,7 @@ pub enum FolderEvent {
/// Only the first level of child views are included. /// Only the first level of child views are included.
#[event(output = "RepeatedViewPB")] #[event(output = "RepeatedViewPB")]
ReadCurrentWorkspaceViews = 40, ReadCurrentWorkspaceViews = 40,
#[event(input = "UpdateViewVisibilityStatusPayloadPB")]
UpdateViewVisibilityStatus = 41,
} }

View File

@ -1120,6 +1120,19 @@ impl FolderManager {
&self.cloud_service &self.cloud_service
} }
pub fn set_views_visibility(&self, view_ids: Vec<String>, is_public: bool) {
self.with_folder(
|| (),
|folder| {
if is_public {
folder.delete_private_view_ids(view_ids);
} else {
folder.add_private_view_ids(view_ids);
}
},
);
}
fn get_sections(&self, section_type: Section) -> Vec<SectionItem> { fn get_sections(&self, section_type: Section) -> Vec<SectionItem> {
self.with_folder(Vec::new, |folder| { self.with_folder(Vec::new, |folder| {
let trash_ids = folder let trash_ids = folder