mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2025-12-28 07:33:45 +00:00
feat: support moveTo feature in more action menu (#6338)
* feat: support moveTo feature in more action menu * fix: unable to switch to another workspace * fix: integration test * chore: update editor version * fix: integration test
This commit is contained in:
parent
522143cfd8
commit
630fdb8995
@ -1,10 +1,5 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import 'package:appflowy/core/config/kv.dart';
|
||||
import 'package:appflowy/core/config/kv_keys.dart';
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
@ -35,6 +30,10 @@ import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/widget/buttons/primary_button.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import 'emoji.dart';
|
||||
@ -572,8 +571,7 @@ extension CommonOperations on WidgetTester {
|
||||
|
||||
Future<void> openMoreViewActions() async {
|
||||
final button = find.byType(MoreViewActions);
|
||||
await tap(button);
|
||||
await pumpAndSettle();
|
||||
await tapButton(button);
|
||||
}
|
||||
|
||||
/// Presses on the Duplicate ViewAction in the [MoreViewActions] popup.
|
||||
@ -581,12 +579,9 @@ extension CommonOperations on WidgetTester {
|
||||
/// [openMoreViewActions] must be called beforehand!
|
||||
///
|
||||
Future<void> duplicateByMoreViewActions() async {
|
||||
final button = find.descendant(
|
||||
of: find.byType(ListView),
|
||||
matching: find.byWidgetPredicate(
|
||||
(widget) =>
|
||||
widget is ViewAction && widget.type == ViewActionType.duplicate,
|
||||
),
|
||||
final button = find.byWidgetPredicate(
|
||||
(widget) =>
|
||||
widget is ViewAction && widget.type == ViewMoreActionType.duplicate,
|
||||
);
|
||||
await tap(button);
|
||||
await pump();
|
||||
@ -601,7 +596,7 @@ extension CommonOperations on WidgetTester {
|
||||
of: find.byType(ListView),
|
||||
matching: find.byWidgetPredicate(
|
||||
(widget) =>
|
||||
widget is ViewAction && widget.type == ViewActionType.delete,
|
||||
widget is ViewAction && widget.type == ViewMoreActionType.delete,
|
||||
),
|
||||
);
|
||||
await tap(button);
|
||||
|
||||
@ -225,11 +225,11 @@ class _HomePageState extends State<_HomePage> {
|
||||
FavoriteBloc()..add(const FavoriteEvent.initial()),
|
||||
),
|
||||
BlocProvider(
|
||||
create: (_) => SpaceBloc()
|
||||
..add(
|
||||
SpaceEvent.initial(
|
||||
widget.userProfile,
|
||||
workspaceId,
|
||||
create: (_) => SpaceBloc(
|
||||
userProfile: widget.userProfile,
|
||||
workspaceId: workspaceId,
|
||||
)..add(
|
||||
const SpaceEvent.initial(
|
||||
openFirstPage: false,
|
||||
),
|
||||
),
|
||||
|
||||
@ -63,11 +63,14 @@ class SidebarSection {
|
||||
/// The [SpaceBloc] is responsible for
|
||||
/// managing the root views in different sections of the workspace.
|
||||
class SpaceBloc extends Bloc<SpaceEvent, SpaceState> {
|
||||
SpaceBloc() : super(SpaceState.initial()) {
|
||||
SpaceBloc({
|
||||
required this.userProfile,
|
||||
required this.workspaceId,
|
||||
}) : super(SpaceState.initial()) {
|
||||
on<SpaceEvent>(
|
||||
(event, emit) async {
|
||||
await event.when(
|
||||
initial: (userProfile, workspaceId, openFirstPage) async {
|
||||
initial: (openFirstPage) async {
|
||||
this.openFirstPage = openFirstPage;
|
||||
|
||||
_initial(userProfile, workspaceId);
|
||||
@ -297,7 +300,7 @@ class SpaceBloc extends Bloc<SpaceEvent, SpaceState> {
|
||||
);
|
||||
},
|
||||
reset: (userProfile, workspaceId, openFirstPage) async {
|
||||
if (workspaceId == _workspaceId) {
|
||||
if (this.workspaceId == workspaceId) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -305,8 +308,6 @@ class SpaceBloc extends Bloc<SpaceEvent, SpaceState> {
|
||||
|
||||
add(
|
||||
SpaceEvent.initial(
|
||||
userProfile,
|
||||
workspaceId,
|
||||
openFirstPage: openFirstPage,
|
||||
),
|
||||
);
|
||||
@ -352,7 +353,7 @@ class SpaceBloc extends Bloc<SpaceEvent, SpaceState> {
|
||||
}
|
||||
|
||||
late WorkspaceService _workspaceService;
|
||||
String? _workspaceId;
|
||||
late String workspaceId;
|
||||
late UserProfilePB userProfile;
|
||||
WorkspaceSectionsListener? _listener;
|
||||
bool openFirstPage = false;
|
||||
@ -442,9 +443,11 @@ class SpaceBloc extends Bloc<SpaceEvent, SpaceState> {
|
||||
}
|
||||
|
||||
void _initial(UserProfilePB userProfile, String workspaceId) {
|
||||
Log.info('initial(or reset) space bloc: $workspaceId, ${userProfile.id}');
|
||||
_workspaceService = WorkspaceService(workspaceId: workspaceId);
|
||||
_workspaceId = workspaceId;
|
||||
|
||||
this.userProfile = userProfile;
|
||||
this.workspaceId = workspaceId;
|
||||
|
||||
_listener = WorkspaceSectionsListener(
|
||||
user: userProfile,
|
||||
@ -464,7 +467,8 @@ class SpaceBloc extends Bloc<SpaceEvent, SpaceState> {
|
||||
_listener?.stop();
|
||||
_listener = null;
|
||||
|
||||
_initial(userProfile, workspaceId);
|
||||
this.userProfile = userProfile;
|
||||
this.workspaceId = workspaceId;
|
||||
}
|
||||
|
||||
Future<ViewPB?> _getLastOpenedSpace(List<ViewPB> spaces) async {
|
||||
@ -522,16 +526,12 @@ class SpaceBloc extends Bloc<SpaceEvent, SpaceState> {
|
||||
}
|
||||
|
||||
Future<bool> migrate({bool auto = true}) async {
|
||||
if (_workspaceId == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
final user =
|
||||
await UserBackendService.getCurrentUserProfile().getOrThrow();
|
||||
final service = UserBackendService(userId: user.id);
|
||||
final members =
|
||||
await service.getWorkspaceMembers(_workspaceId!).getOrThrow();
|
||||
await service.getWorkspaceMembers(workspaceId).getOrThrow();
|
||||
final isOwner = members.items
|
||||
.any((e) => e.role == AFRolePB.Owner && e.email == user.email);
|
||||
|
||||
@ -563,7 +563,7 @@ class SpaceBloc extends Bloc<SpaceEvent, SpaceState> {
|
||||
}
|
||||
|
||||
final viewId = fixedUuid(
|
||||
user.id.toInt() + (_workspaceId?.hashCode ?? 0),
|
||||
user.id.toInt() + workspaceId.hashCode,
|
||||
UuidType.publicSpace,
|
||||
);
|
||||
final publicSpace = await _createSpace(
|
||||
@ -702,9 +702,7 @@ class SpaceBloc extends Bloc<SpaceEvent, SpaceState> {
|
||||
|
||||
@freezed
|
||||
class SpaceEvent with _$SpaceEvent {
|
||||
const factory SpaceEvent.initial(
|
||||
UserProfilePB userProfile,
|
||||
String workspaceId, {
|
||||
const factory SpaceEvent.initial({
|
||||
required bool openFirstPage,
|
||||
}) = _Initial;
|
||||
const factory SpaceEvent.create({
|
||||
|
||||
@ -7,7 +7,6 @@ import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/shared_w
|
||||
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_backend/protobuf/flowy-user/protobuf.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
@ -20,14 +19,10 @@ 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 MovePageMenuOnSelected onSelected;
|
||||
|
||||
@override
|
||||
@ -47,25 +42,11 @@ class _MovePageMenuState extends State<MovePageMenu> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MultiBlocProvider(
|
||||
providers: [
|
||||
BlocProvider(
|
||||
create: (context) => SpaceBloc()
|
||||
..add(
|
||||
SpaceEvent.initial(
|
||||
widget.userProfile,
|
||||
widget.workspaceId,
|
||||
openFirstPage: false,
|
||||
),
|
||||
),
|
||||
return BlocProvider(
|
||||
create: (context) => SpaceSearchBloc()
|
||||
..add(
|
||||
const SpaceSearchEvent.initial(),
|
||||
),
|
||||
BlocProvider(
|
||||
create: (context) => SpaceSearchBloc()
|
||||
..add(
|
||||
const SpaceSearchEvent.initial(),
|
||||
),
|
||||
),
|
||||
],
|
||||
child: BlocBuilder<SpaceBloc, SpaceState>(
|
||||
builder: (context, state) {
|
||||
final space = state.currentSpace;
|
||||
|
||||
@ -116,11 +116,11 @@ class HomeSideBar extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
BlocProvider(
|
||||
create: (_) => SpaceBloc()
|
||||
..add(
|
||||
SpaceEvent.initial(
|
||||
userProfile,
|
||||
workspaceId,
|
||||
create: (_) => SpaceBloc(
|
||||
userProfile: userProfile,
|
||||
workspaceId: workspaceId,
|
||||
)..add(
|
||||
const SpaceEvent.initial(
|
||||
openFirstPage: false,
|
||||
),
|
||||
),
|
||||
|
||||
@ -5,7 +5,6 @@ import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/shared/sidebar_setting.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_icon.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_menu.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/toast.dart';
|
||||
import 'package:appflowy/workspace/presentation/notifications/widgets/notification_button.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
@ -16,6 +15,7 @@ import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:toastification/toastification.dart';
|
||||
|
||||
class SidebarWorkspace extends StatefulWidget {
|
||||
const SidebarWorkspace({super.key, required this.userProfile});
|
||||
@ -170,7 +170,14 @@ class _SidebarWorkspaceState extends State<SidebarWorkspace> {
|
||||
}
|
||||
|
||||
if (message != null) {
|
||||
showSnackBarMessage(context, message);
|
||||
showToastNotification(
|
||||
context,
|
||||
message: message,
|
||||
type: result.fold(
|
||||
(_) => ToastificationType.success,
|
||||
(_) => ToastificationType.error,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -335,7 +335,7 @@ class _InnerViewItemState extends State<InnerViewItem> {
|
||||
_isDragging = isDragging;
|
||||
},
|
||||
onMove: widget.isPlaceholder
|
||||
? (from, to) => _moveViewCrossSection(
|
||||
? (from, to) => moveViewCrossSpace(
|
||||
context,
|
||||
null,
|
||||
widget.view,
|
||||
@ -766,7 +766,7 @@ class _SingleInnerViewItemState extends State<SingleInnerViewItem> {
|
||||
}
|
||||
final space = value.$1;
|
||||
final target = value.$2;
|
||||
_moveViewCrossSection(
|
||||
moveViewCrossSpace(
|
||||
context,
|
||||
space,
|
||||
widget.view,
|
||||
@ -827,7 +827,7 @@ bool isReferencedDatabaseView(ViewPB view, ViewPB? parentView) {
|
||||
return view.layout.isDatabaseView && parentView.layout.isDatabaseView;
|
||||
}
|
||||
|
||||
void _moveViewCrossSection(
|
||||
void moveViewCrossSpace(
|
||||
BuildContext context,
|
||||
ViewPB? toSpace,
|
||||
ViewPB view,
|
||||
|
||||
@ -122,13 +122,19 @@ class ViewMoreActionTypeWrapper extends CustomActionCell {
|
||||
ViewMoreActionTypeWrapper(
|
||||
this.inner,
|
||||
this.sourceView,
|
||||
this.onTap,
|
||||
);
|
||||
this.onTap, {
|
||||
this.moveActionDirection,
|
||||
this.moveActionOffset,
|
||||
});
|
||||
|
||||
final ViewMoreActionType inner;
|
||||
final ViewPB sourceView;
|
||||
final void Function(PopoverController controller, dynamic data) onTap;
|
||||
|
||||
// custom the move to action button
|
||||
final PopoverDirection? moveActionDirection;
|
||||
final Offset? moveActionOffset;
|
||||
|
||||
@override
|
||||
Widget buildWithContext(BuildContext context, PopoverController controller) {
|
||||
if (inner == ViewMoreActionType.divider) {
|
||||
@ -174,9 +180,12 @@ class ViewMoreActionTypeWrapper extends CustomActionCell {
|
||||
BuildContext context,
|
||||
PopoverController controller,
|
||||
) {
|
||||
// move to feature doesn't support in local mode
|
||||
if (context.read<SpaceBloc>().state.spaces.isEmpty) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
final child = _buildActionButton(context, null);
|
||||
final userProfile = context.read<SpaceBloc>().userProfile;
|
||||
final workspaceId = context.read<SpaceBloc>().state.currentSpace?.id;
|
||||
|
||||
return AppFlowyPopover(
|
||||
constraints: const BoxConstraints(
|
||||
@ -188,18 +197,17 @@ class ViewMoreActionTypeWrapper extends CustomActionCell {
|
||||
vertical: 12.0,
|
||||
),
|
||||
clickHandler: PopoverClickHandler.gestureDetector,
|
||||
direction: moveActionDirection ?? PopoverDirection.rightWithTopAligned,
|
||||
offset: moveActionOffset,
|
||||
popupBuilder: (_) {
|
||||
if (workspaceId == null) {
|
||||
return const SizedBox();
|
||||
}
|
||||
|
||||
return MovePageMenu(
|
||||
sourceView: sourceView,
|
||||
userProfile: userProfile,
|
||||
workspaceId: workspaceId,
|
||||
onSelected: (space, view) {
|
||||
onTap(controller, (space, view));
|
||||
},
|
||||
return BlocProvider.value(
|
||||
value: context.read<SpaceBloc>(),
|
||||
child: MovePageMenu(
|
||||
sourceView: sourceView,
|
||||
onSelected: (space, view) {
|
||||
onTap(controller, (space, view));
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
child: child,
|
||||
|
||||
@ -1,12 +1,16 @@
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart';
|
||||
import 'package:appflowy/workspace/application/sidebar/space/space_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/view/view_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/view_info/view_info_bloc.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/view/view_action_type.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/more_view_actions/widgets/common_view_action.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/more_view_actions/widgets/font_size_action.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/more_view_actions/widgets/view_meta_info.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-user/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';
|
||||
@ -42,74 +46,121 @@ class _MoreViewActionsState extends State<MoreViewActions> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final appearanceSettings = context.watch<AppearanceSettingsCubit>().state;
|
||||
final dateFormat = appearanceSettings.dateFormat;
|
||||
final timeFormat = appearanceSettings.timeFormat;
|
||||
|
||||
return BlocBuilder<ViewInfoBloc, ViewInfoState>(
|
||||
builder: (context, state) {
|
||||
return AppFlowyPopover(
|
||||
mutex: popoverMutex,
|
||||
constraints: BoxConstraints.loose(const Size(215, 400)),
|
||||
offset: const Offset(0, 30),
|
||||
popupBuilder: (_) {
|
||||
final actions = [
|
||||
if (widget.isDocument) ...[
|
||||
const FontSizeAction(),
|
||||
const Divider(height: 4),
|
||||
],
|
||||
...ViewActionType.values.map(
|
||||
(type) => ViewAction(
|
||||
type: type,
|
||||
view: widget.view,
|
||||
mutex: popoverMutex,
|
||||
),
|
||||
),
|
||||
if (state.documentCounters != null ||
|
||||
state.createdAt != null) ...[
|
||||
const Divider(height: 4),
|
||||
ViewMetaInfo(
|
||||
dateFormat: dateFormat,
|
||||
timeFormat: timeFormat,
|
||||
documentCounters: state.documentCounters,
|
||||
createdAt: state.createdAt,
|
||||
),
|
||||
],
|
||||
];
|
||||
|
||||
return BlocProvider(
|
||||
create: (_) =>
|
||||
ViewBloc(view: widget.view)..add(const ViewEvent.initial()),
|
||||
child: ListView.separated(
|
||||
shrinkWrap: true,
|
||||
padding: EdgeInsets.zero,
|
||||
itemCount: actions.length,
|
||||
separatorBuilder: (_, __) => const VSpace(4),
|
||||
physics: StyledScrollPhysics(),
|
||||
itemBuilder: (_, index) => actions[index],
|
||||
),
|
||||
);
|
||||
},
|
||||
child: FlowyTooltip(
|
||||
message: LocaleKeys.moreAction_moreOptions.tr(),
|
||||
child: FlowyHover(
|
||||
style: HoverStyle(
|
||||
foregroundColorOnHover: Theme.of(context).colorScheme.onPrimary,
|
||||
),
|
||||
builder: (context, isHovering) => Padding(
|
||||
padding: const EdgeInsets.all(6),
|
||||
child: FlowySvg(
|
||||
FlowySvgs.three_dots_s,
|
||||
size: const Size.square(18),
|
||||
color: isHovering
|
||||
? Theme.of(context).colorScheme.onSurface
|
||||
: Theme.of(context).iconTheme.color,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
constraints: const BoxConstraints(maxWidth: 220),
|
||||
offset: const Offset(0, 42),
|
||||
popupBuilder: (_) => _buildPopup(state),
|
||||
child: const _ThreeDots(),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPopup(ViewInfoState state) {
|
||||
final userWorkspaceBloc = context.read<UserWorkspaceBloc>();
|
||||
final userProfile = userWorkspaceBloc.userProfile;
|
||||
final workspaceId =
|
||||
userWorkspaceBloc.state.currentWorkspace?.workspaceId ?? '';
|
||||
final actions = _buildActions(state);
|
||||
|
||||
return MultiBlocProvider(
|
||||
providers: [
|
||||
BlocProvider(
|
||||
create: (_) =>
|
||||
ViewBloc(view: widget.view)..add(const ViewEvent.initial()),
|
||||
),
|
||||
BlocProvider(
|
||||
create: (context) => SpaceBloc(
|
||||
userProfile: userProfile,
|
||||
workspaceId: workspaceId,
|
||||
)..add(
|
||||
const SpaceEvent.initial(openFirstPage: false),
|
||||
),
|
||||
),
|
||||
],
|
||||
child: BlocBuilder<SpaceBloc, SpaceState>(
|
||||
builder: (context, state) {
|
||||
if (state.spaces.isEmpty &&
|
||||
userProfile.authenticator == AuthenticatorPB.AppFlowyCloud) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return ListView.builder(
|
||||
key: ValueKey(state.spaces.hashCode),
|
||||
shrinkWrap: true,
|
||||
padding: EdgeInsets.zero,
|
||||
itemCount: actions.length,
|
||||
physics: StyledScrollPhysics(),
|
||||
itemBuilder: (_, index) => actions[index],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
List<Widget> _buildActions(ViewInfoState state) {
|
||||
final appearanceSettings = context.watch<AppearanceSettingsCubit>().state;
|
||||
final dateFormat = appearanceSettings.dateFormat;
|
||||
final timeFormat = appearanceSettings.timeFormat;
|
||||
|
||||
final viewMoreActionTypes = [
|
||||
if (widget.isDocument) ViewMoreActionType.divider,
|
||||
ViewMoreActionType.duplicate,
|
||||
ViewMoreActionType.moveTo,
|
||||
ViewMoreActionType.delete,
|
||||
ViewMoreActionType.divider,
|
||||
];
|
||||
|
||||
final actions = [
|
||||
if (widget.isDocument) ...[
|
||||
const FontSizeAction(),
|
||||
],
|
||||
...viewMoreActionTypes.map(
|
||||
(type) => ViewAction(
|
||||
type: type,
|
||||
view: widget.view,
|
||||
mutex: popoverMutex,
|
||||
),
|
||||
),
|
||||
if (state.documentCounters != null || state.createdAt != null) ...[
|
||||
ViewMetaInfo(
|
||||
dateFormat: dateFormat,
|
||||
timeFormat: timeFormat,
|
||||
documentCounters: state.documentCounters,
|
||||
createdAt: state.createdAt,
|
||||
),
|
||||
const VSpace(4.0),
|
||||
],
|
||||
];
|
||||
return actions;
|
||||
}
|
||||
}
|
||||
|
||||
class _ThreeDots extends StatelessWidget {
|
||||
const _ThreeDots();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FlowyTooltip(
|
||||
message: LocaleKeys.moreAction_moreOptions.tr(),
|
||||
child: FlowyHover(
|
||||
style: HoverStyle(
|
||||
foregroundColorOnHover: Theme.of(context).colorScheme.onPrimary,
|
||||
),
|
||||
builder: (context, isHovering) => Padding(
|
||||
padding: const EdgeInsets.all(6),
|
||||
child: FlowySvg(
|
||||
FlowySvgs.three_dots_s,
|
||||
size: const Size.square(18),
|
||||
color: isHovering
|
||||
? Theme.of(context).colorScheme.onSurface
|
||||
: Theme.of(context).iconTheme.color,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,37 +1,18 @@
|
||||
import 'package:appflowy/generated/flowy_svgs.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/view/view_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/view/view_service.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/view/view_action_type.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/view/view_more_action_button.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
|
||||
import 'package:appflowy_backend/log.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:flowy_infra/theme_extension.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/button.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/text.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
enum ViewActionType {
|
||||
delete,
|
||||
duplicate;
|
||||
|
||||
String get label => switch (this) {
|
||||
ViewActionType.delete => LocaleKeys.moreAction_deleteView.tr(),
|
||||
ViewActionType.duplicate => LocaleKeys.moreAction_duplicateView.tr(),
|
||||
};
|
||||
|
||||
FlowySvgData get icon => switch (this) {
|
||||
ViewActionType.delete => FlowySvgs.delete_s,
|
||||
ViewActionType.duplicate => FlowySvgs.m_duplicate_s,
|
||||
};
|
||||
|
||||
ViewEvent get actionEvent => switch (this) {
|
||||
ViewActionType.delete => const ViewEvent.delete(),
|
||||
ViewActionType.duplicate => const ViewEvent.duplicate(),
|
||||
};
|
||||
}
|
||||
|
||||
class ViewAction extends StatelessWidget {
|
||||
const ViewAction({
|
||||
super.key,
|
||||
@ -40,49 +21,77 @@ class ViewAction extends StatelessWidget {
|
||||
this.mutex,
|
||||
});
|
||||
|
||||
final ViewActionType type;
|
||||
final ViewMoreActionType type;
|
||||
final ViewPB view;
|
||||
final PopoverMutex? mutex;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FlowyButton(
|
||||
onTap: () async {
|
||||
await _onAction(context);
|
||||
final wrapper = ViewMoreActionTypeWrapper(
|
||||
type,
|
||||
view,
|
||||
(controller, data) async {
|
||||
await _onAction(context, data);
|
||||
mutex?.close();
|
||||
},
|
||||
text: FlowyText.regular(
|
||||
type.label,
|
||||
color: AFThemeExtension.of(context).textColor,
|
||||
),
|
||||
leftIcon: FlowySvg(
|
||||
type.icon,
|
||||
color: Theme.of(context).iconTheme.color,
|
||||
size: const Size.square(18),
|
||||
),
|
||||
leftIconSize: const Size(18, 18),
|
||||
hoverColor: AFThemeExtension.of(context).lightGreyHover,
|
||||
moveActionDirection: PopoverDirection.leftWithTopAligned,
|
||||
moveActionOffset: const Offset(-10, 0),
|
||||
);
|
||||
return wrapper.buildWithContext(
|
||||
context,
|
||||
// this is a dummy controller, we don't need to control the popover here.
|
||||
PopoverController(),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _onAction(BuildContext context) async {
|
||||
if (type == ViewActionType.delete) {
|
||||
final (containPublishedPage, _) =
|
||||
await ViewBackendService.containPublishedPage(view);
|
||||
if (containPublishedPage && context.mounted) {
|
||||
await showConfirmDeletionDialog(
|
||||
context: context,
|
||||
name: view.name,
|
||||
description: LocaleKeys.publish_containsPublishedPage.tr(),
|
||||
onConfirm: () {
|
||||
context.read<ViewBloc>().add(const ViewEvent.delete());
|
||||
},
|
||||
Future<void> _onAction(
|
||||
BuildContext context,
|
||||
dynamic data,
|
||||
) async {
|
||||
switch (type) {
|
||||
case ViewMoreActionType.delete:
|
||||
final (containPublishedPage, _) =
|
||||
await ViewBackendService.containPublishedPage(view);
|
||||
|
||||
if (containPublishedPage && context.mounted) {
|
||||
await showConfirmDeletionDialog(
|
||||
context: context,
|
||||
name: view.name,
|
||||
description: LocaleKeys.publish_containsPublishedPage.tr(),
|
||||
onConfirm: () {
|
||||
context.read<ViewBloc>().add(const ViewEvent.delete());
|
||||
},
|
||||
);
|
||||
} else if (context.mounted) {
|
||||
context.read<ViewBloc>().add(const ViewEvent.delete());
|
||||
}
|
||||
case ViewMoreActionType.duplicate:
|
||||
context.read<ViewBloc>().add(const ViewEvent.duplicate());
|
||||
case ViewMoreActionType.moveTo:
|
||||
final value = data;
|
||||
if (value is! (ViewPB, ViewPB)) {
|
||||
return;
|
||||
}
|
||||
final space = value.$1;
|
||||
final target = value.$2;
|
||||
final result = await ViewBackendService.getView(view.parentViewId);
|
||||
result.fold(
|
||||
(parentView) => moveViewCrossSpace(
|
||||
context,
|
||||
space,
|
||||
view,
|
||||
parentView,
|
||||
FolderSpaceType.public,
|
||||
view,
|
||||
target.id,
|
||||
),
|
||||
(f) => Log.error(f),
|
||||
);
|
||||
} else if (context.mounted) {
|
||||
context.read<ViewBloc>().add(const ViewEvent.delete());
|
||||
}
|
||||
} else {
|
||||
context.read<ViewBloc>().add(type.actionEvent);
|
||||
|
||||
// the move action is handled in the button itself
|
||||
break;
|
||||
default:
|
||||
throw UnimplementedError();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,5 +1,3 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/document/application/document_appearance_cubit.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/more_view_actions/widgets/font_size_stepper.dart';
|
||||
@ -7,6 +5,7 @@ import 'package:appflowy_popover/appflowy_popover.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra/theme_extension.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
class FontSizeAction extends StatelessWidget {
|
||||
@ -31,18 +30,25 @@ class FontSizeAction extends StatelessWidget {
|
||||
),
|
||||
);
|
||||
},
|
||||
child: FlowyButton(
|
||||
text: FlowyText.regular(
|
||||
LocaleKeys.moreAction_fontSize.tr(),
|
||||
color: AFThemeExtension.of(context).textColor,
|
||||
child: Container(
|
||||
height: 34,
|
||||
padding: const EdgeInsets.symmetric(vertical: 2.0),
|
||||
child: FlowyButton(
|
||||
text: FlowyText.regular(
|
||||
LocaleKeys.moreAction_fontSize.tr(),
|
||||
fontSize: 14.0,
|
||||
lineHeight: 1.0,
|
||||
figmaLineHeight: 18.0,
|
||||
color: AFThemeExtension.of(context).textColor,
|
||||
),
|
||||
leftIcon: Icon(
|
||||
Icons.format_size_sharp,
|
||||
color: Theme.of(context).iconTheme.color,
|
||||
size: 18,
|
||||
),
|
||||
leftIconSize: const Size(18, 18),
|
||||
hoverColor: AFThemeExtension.of(context).lightGreyHover,
|
||||
),
|
||||
leftIcon: Icon(
|
||||
Icons.format_size_sharp,
|
||||
color: Theme.of(context).iconTheme.color,
|
||||
size: 18,
|
||||
),
|
||||
leftIconSize: const Size(18, 18),
|
||||
hoverColor: AFThemeExtension.of(context).lightGreyHover,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,5 +1,3 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/workspace/application/settings/date_time/date_format_ext.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-user/date_time.pbenum.dart';
|
||||
@ -7,6 +5,7 @@ import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/text.dart';
|
||||
import 'package:flowy_infra_ui/widget/spacing.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class ViewMetaInfo extends StatelessWidget {
|
||||
const ViewMetaInfo({
|
||||
@ -39,7 +38,7 @@ class ViewMetaInfo extends StatelessWidget {
|
||||
numberFormat.format(documentCounters!.wordCount).toString(),
|
||||
],
|
||||
),
|
||||
fontSize: 11,
|
||||
fontSize: 12,
|
||||
color: Theme.of(context).hintColor,
|
||||
),
|
||||
const VSpace(2),
|
||||
@ -49,7 +48,7 @@ class ViewMetaInfo extends StatelessWidget {
|
||||
numberFormat.format(documentCounters!.charCount).toString(),
|
||||
],
|
||||
),
|
||||
fontSize: 11,
|
||||
fontSize: 12,
|
||||
color: Theme.of(context).hintColor,
|
||||
),
|
||||
],
|
||||
@ -59,7 +58,7 @@ class ViewMetaInfo extends StatelessWidget {
|
||||
LocaleKeys.moreAction_createdAt.tr(
|
||||
args: [dateFormat.formatDate(createdAt!, true, timeFormat)],
|
||||
),
|
||||
fontSize: 11,
|
||||
fontSize: 12,
|
||||
maxLines: 2,
|
||||
color: Theme.of(context).hintColor,
|
||||
),
|
||||
|
||||
@ -53,8 +53,8 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
path: "."
|
||||
ref: bb525e4
|
||||
resolved-ref: bb525e41dfda40c891c2641d38bce29eb1a21769
|
||||
ref: "0a0154ff7d10d13de755623e56527aa4bed26241"
|
||||
resolved-ref: "0a0154ff7d10d13de755623e56527aa4bed26241"
|
||||
url: "https://github.com/AppFlowy-IO/appflowy-editor.git"
|
||||
source: git
|
||||
version: "3.3.0"
|
||||
|
||||
@ -175,7 +175,7 @@ dependency_overrides:
|
||||
appflowy_editor:
|
||||
git:
|
||||
url: https://github.com/AppFlowy-IO/appflowy-editor.git
|
||||
ref: "bb525e4"
|
||||
ref: "0a0154ff7d10d13de755623e56527aa4bed26241"
|
||||
|
||||
appflowy_editor_plugins:
|
||||
git:
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user