mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
feat: support moving view across sections (#5015)
This commit is contained in:
parent
893d23d6a3
commit
723423d423
@ -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
|
||||||
|
@ -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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
@ -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);
|
||||||
|
|
||||||
|
@ -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,
|
||||||
|
@ -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
|
||||||
|
@ -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(())
|
||||||
|
}
|
||||||
|
@ -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,
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
Loading…
Reference in New Issue
Block a user