mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
feat: mobile sort editor (#4714)
This commit is contained in:
parent
acd75befbc
commit
c0796e8ae5
@ -8,14 +8,17 @@ class AppBarBackButton extends StatelessWidget {
|
||||
const AppBarBackButton({
|
||||
super.key,
|
||||
this.onTap,
|
||||
this.padding,
|
||||
});
|
||||
|
||||
final VoidCallback? onTap;
|
||||
final EdgeInsetsGeometry? padding;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AppBarButton(
|
||||
onTap: onTap ?? () => Navigator.pop(context),
|
||||
padding: padding,
|
||||
child: const FlowySvg(
|
||||
FlowySvgs.m_app_bar_back_s,
|
||||
),
|
||||
@ -73,8 +76,8 @@ class AppBarDoneButton extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AppBarButton(
|
||||
isActionButton: true,
|
||||
onTap: onTap,
|
||||
padding: const EdgeInsets.fromLTRB(12, 12, 8, 12),
|
||||
child: FlowyText(
|
||||
LocaleKeys.button_Done.tr(),
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
@ -85,6 +88,39 @@ class AppBarDoneButton extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class AppBarSaveButton extends StatelessWidget {
|
||||
const AppBarSaveButton({
|
||||
super.key,
|
||||
required this.onTap,
|
||||
this.enable = true,
|
||||
this.padding = const EdgeInsets.fromLTRB(12, 12, 8, 12),
|
||||
});
|
||||
|
||||
final VoidCallback onTap;
|
||||
final bool enable;
|
||||
final EdgeInsetsGeometry padding;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AppBarButton(
|
||||
onTap: () {
|
||||
if (enable) {
|
||||
onTap();
|
||||
}
|
||||
},
|
||||
padding: padding,
|
||||
child: FlowyText(
|
||||
LocaleKeys.button_save.tr(),
|
||||
color: enable
|
||||
? Theme.of(context).colorScheme.primary
|
||||
: Theme.of(context).disabledColor,
|
||||
fontWeight: FontWeight.w500,
|
||||
textAlign: TextAlign.right,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class AppBarFilledDoneButton extends StatelessWidget {
|
||||
const AppBarFilledDoneButton({super.key, required this.onTap});
|
||||
|
||||
@ -129,7 +165,7 @@ class AppBarMoreButton extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AppBarButton(
|
||||
isActionButton: true,
|
||||
padding: const EdgeInsets.fromLTRB(12, 12, 8, 12),
|
||||
onTap: () => onTap(context),
|
||||
child: const FlowySvg(FlowySvgs.three_dots_s),
|
||||
);
|
||||
@ -139,14 +175,14 @@ class AppBarMoreButton extends StatelessWidget {
|
||||
class AppBarButton extends StatelessWidget {
|
||||
const AppBarButton({
|
||||
super.key,
|
||||
this.isActionButton = false,
|
||||
required this.onTap,
|
||||
required this.child,
|
||||
this.padding,
|
||||
});
|
||||
|
||||
final VoidCallback onTap;
|
||||
final Widget child;
|
||||
final bool isActionButton;
|
||||
final EdgeInsetsGeometry? padding;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@ -154,12 +190,7 @@ class AppBarButton extends StatelessWidget {
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: onTap,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(
|
||||
top: 12.0,
|
||||
bottom: 12.0,
|
||||
left: 12.0,
|
||||
right: isActionButton ? 12.0 : 8.0,
|
||||
),
|
||||
padding: padding ?? const EdgeInsets.all(12),
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
|
@ -0,0 +1,555 @@
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/mobile/presentation/base/app_bar_actions.dart';
|
||||
import 'package:appflowy/mobile/presentation/widgets/widgets.dart';
|
||||
import 'package:appflowy/plugins/database/grid/application/sort/sort_editor_bloc.dart';
|
||||
import 'package:appflowy/plugins/database/grid/presentation/widgets/sort/sort_info.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra/size.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:fluttertoast/fluttertoast.dart';
|
||||
|
||||
import 'database_sort_bottom_sheet_cubit.dart';
|
||||
|
||||
class MobileSortEditor extends StatefulWidget {
|
||||
const MobileSortEditor({
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
State<MobileSortEditor> createState() => _MobileSortEditorState();
|
||||
}
|
||||
|
||||
class _MobileSortEditorState extends State<MobileSortEditor> {
|
||||
final PageController _pageController = PageController();
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_pageController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider(
|
||||
create: (context) => MobileSortEditorCubit(
|
||||
pageController: _pageController,
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
const _Header(),
|
||||
SizedBox(
|
||||
height: 400, //314,
|
||||
child: PageView.builder(
|
||||
controller: _pageController,
|
||||
itemCount: 2,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemBuilder: (context, index) {
|
||||
return index == 0
|
||||
? Padding(
|
||||
padding: EdgeInsets.only(
|
||||
bottom: MediaQuery.of(context).padding.bottom,
|
||||
),
|
||||
child: const _Overview(),
|
||||
)
|
||||
: const _SortDetail();
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _Header extends StatelessWidget {
|
||||
const _Header();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<MobileSortEditorCubit, MobileSortEditorState>(
|
||||
builder: (context, state) {
|
||||
return SizedBox(
|
||||
height: 44.0,
|
||||
child: Stack(
|
||||
children: [
|
||||
if (state.showBackButton)
|
||||
Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: AppBarBackButton(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 12,
|
||||
horizontal: 16,
|
||||
),
|
||||
onTap: () => context
|
||||
.read<MobileSortEditorCubit>()
|
||||
.returnToOverview(),
|
||||
),
|
||||
),
|
||||
Align(
|
||||
child: FlowyText.medium(
|
||||
LocaleKeys.grid_settings_sort.tr(),
|
||||
fontSize: 16.0,
|
||||
),
|
||||
),
|
||||
if (state.isCreatingNewSort)
|
||||
Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: AppBarSaveButton(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 12,
|
||||
horizontal: 16,
|
||||
),
|
||||
enable: state.newSortFieldId != null,
|
||||
onTap: () {
|
||||
_tryCreateSort(context, state);
|
||||
context.read<MobileSortEditorCubit>().returnToOverview();
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void _tryCreateSort(BuildContext context, MobileSortEditorState state) {
|
||||
if (state.newSortFieldId != null && state.newSortCondition != null) {
|
||||
context.read<SortEditorBloc>().add(
|
||||
SortEditorEvent.createSort(
|
||||
state.newSortFieldId!,
|
||||
state.newSortCondition!,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class _Overview extends StatelessWidget {
|
||||
const _Overview();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<SortEditorBloc, SortEditorState>(
|
||||
builder: (context, state) {
|
||||
return Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: state.sortInfos.isEmpty
|
||||
? Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
FlowySvg(
|
||||
FlowySvgs.sort_descending_s,
|
||||
size: const Size.square(60),
|
||||
color: Theme.of(context).hintColor,
|
||||
),
|
||||
FlowyText(
|
||||
LocaleKeys.grid_sort_empty.tr(),
|
||||
color: Theme.of(context).hintColor,
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
: ReorderableListView.builder(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
),
|
||||
proxyDecorator: (child, index, animation) => Material(
|
||||
color: Colors.transparent,
|
||||
child: child,
|
||||
),
|
||||
onReorder: (oldIndex, newIndex) => context
|
||||
.read<SortEditorBloc>()
|
||||
.add(SortEditorEvent.reorderSort(oldIndex, newIndex)),
|
||||
itemCount: state.sortInfos.length,
|
||||
itemBuilder: (context, index) => _SortItem(
|
||||
key: ValueKey("sort_item_$index"),
|
||||
sort: state.sortInfos[index],
|
||||
),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
height: 44,
|
||||
width: double.infinity,
|
||||
margin: const EdgeInsets.symmetric(horizontal: 14),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.fromBorderSide(
|
||||
BorderSide(
|
||||
width: 0.5,
|
||||
color: Theme.of(context).dividerColor,
|
||||
),
|
||||
),
|
||||
borderRadius: Corners.s10Border,
|
||||
),
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
final firstField = context
|
||||
.read<SortEditorBloc>()
|
||||
.state
|
||||
.creatableFields
|
||||
.firstOrNull;
|
||||
if (firstField == null) {
|
||||
Fluttertoast.showToast(
|
||||
msg: LocaleKeys.grid_sort_cannotFindCreatableField.tr(),
|
||||
gravity: ToastGravity.BOTTOM,
|
||||
);
|
||||
} else {
|
||||
context.read<MobileSortEditorCubit>().startCreatingSort();
|
||||
}
|
||||
},
|
||||
borderRadius: Corners.s10Border,
|
||||
child: Center(
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const FlowySvg(
|
||||
FlowySvgs.add_s,
|
||||
size: Size.square(16),
|
||||
),
|
||||
const HSpace(6.0),
|
||||
FlowyText(
|
||||
LocaleKeys.grid_sort_addSort.tr(),
|
||||
fontSize: 15,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _SortItem extends StatelessWidget {
|
||||
const _SortItem({super.key, required this.sort});
|
||||
|
||||
final SortInfo sort;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.symmetric(
|
||||
vertical: 4.0,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).hoverColor,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Stack(
|
||||
children: [
|
||||
GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: () => context
|
||||
.read<MobileSortEditorCubit>()
|
||||
.startEditingSort(sort.sortId),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 14,
|
||||
horizontal: 8,
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12.0),
|
||||
child: Expanded(
|
||||
child: FlowyText.medium(
|
||||
LocaleKeys.grid_sort_by.tr(),
|
||||
fontSize: 15,
|
||||
),
|
||||
),
|
||||
),
|
||||
const VSpace(10),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Container(
|
||||
height: 44,
|
||||
decoration: BoxDecoration(
|
||||
border: Border.fromBorderSide(
|
||||
BorderSide(
|
||||
width: 0.5,
|
||||
color: Theme.of(context).dividerColor,
|
||||
),
|
||||
),
|
||||
borderRadius: Corners.s10Border,
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
child: Center(
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: FlowyText(
|
||||
sort.fieldInfo.name,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
const HSpace(6.0),
|
||||
FlowySvg(
|
||||
FlowySvgs.icon_right_small_ccm_outlined_s,
|
||||
size: const Size.square(14),
|
||||
color: Theme.of(context).hintColor,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const HSpace(6),
|
||||
Expanded(
|
||||
child: Container(
|
||||
height: 44,
|
||||
decoration: BoxDecoration(
|
||||
border: Border.fromBorderSide(
|
||||
BorderSide(
|
||||
width: 0.5,
|
||||
color: Theme.of(context).dividerColor,
|
||||
),
|
||||
),
|
||||
borderRadius: Corners.s10Border,
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
),
|
||||
padding: const EdgeInsetsDirectional.only(
|
||||
start: 12,
|
||||
end: 10,
|
||||
),
|
||||
child: Center(
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: FlowyText(
|
||||
sort.sortPB.condition.name,
|
||||
),
|
||||
),
|
||||
const HSpace(6.0),
|
||||
FlowySvg(
|
||||
FlowySvgs.icon_right_small_ccm_outlined_s,
|
||||
size: const Size.square(14),
|
||||
color: Theme.of(context).hintColor,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
right: 8,
|
||||
top: 9,
|
||||
child: InkWell(
|
||||
onTap: () => context
|
||||
.read<SortEditorBloc>()
|
||||
.add(SortEditorEvent.deleteSort(sort)),
|
||||
// steal from the container LongClickReorderWidget thing
|
||||
onLongPress: () {},
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
child: SizedBox.square(
|
||||
dimension: 34,
|
||||
child: Center(
|
||||
child: FlowySvg(
|
||||
FlowySvgs.trash_m,
|
||||
size: const Size.square(18),
|
||||
color: Theme.of(context).hintColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _SortDetail extends StatelessWidget {
|
||||
const _SortDetail();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isCreatingNewSort =
|
||||
context.read<MobileSortEditorCubit>().state.isCreatingNewSort;
|
||||
|
||||
return isCreatingNewSort
|
||||
? const _SortDetailContent()
|
||||
: BlocSelector<SortEditorBloc, SortEditorState, SortInfo>(
|
||||
selector: (state) => state.sortInfos.firstWhere(
|
||||
(sortInfo) =>
|
||||
sortInfo.sortId ==
|
||||
context.read<MobileSortEditorCubit>().state.editingSortId,
|
||||
),
|
||||
builder: (context, sortInfo) {
|
||||
return _SortDetailContent(sortInfo: sortInfo);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _SortDetailContent extends StatelessWidget {
|
||||
const _SortDetailContent({
|
||||
this.sortInfo,
|
||||
});
|
||||
|
||||
final SortInfo? sortInfo;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const VSpace(4),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: DefaultTabController(
|
||||
length: 2,
|
||||
initialIndex: sortInfo == null
|
||||
? 0
|
||||
: sortInfo!.sortPB.condition == SortConditionPB.Ascending
|
||||
? 0
|
||||
: 1,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(3.0),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
color: Theme.of(context).hoverColor,
|
||||
),
|
||||
child: TabBar(
|
||||
indicatorSize: TabBarIndicatorSize.label,
|
||||
labelPadding: EdgeInsets.zero,
|
||||
padding: EdgeInsets.zero,
|
||||
indicatorWeight: 0,
|
||||
indicator: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
),
|
||||
splashFactory: NoSplash.splashFactory,
|
||||
overlayColor: const MaterialStatePropertyAll(
|
||||
Colors.transparent,
|
||||
),
|
||||
onTap: (index) {
|
||||
final newCondition = index == 0
|
||||
? SortConditionPB.Ascending
|
||||
: SortConditionPB.Descending;
|
||||
_changeCondition(context, newCondition);
|
||||
},
|
||||
tabs: [
|
||||
Tab(
|
||||
height: 34,
|
||||
child: Center(
|
||||
child: FlowyText(
|
||||
LocaleKeys.grid_sort_ascending.tr(),
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
),
|
||||
Tab(
|
||||
height: 34,
|
||||
child: Center(
|
||||
child: FlowyText(
|
||||
LocaleKeys.grid_sort_descending.tr(),
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const VSpace(20),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
child: FlowyText(
|
||||
LocaleKeys.grid_settings_sortBy.tr().toUpperCase(),
|
||||
fontSize: 13,
|
||||
color: Theme.of(context).hintColor,
|
||||
),
|
||||
),
|
||||
const VSpace(4.0),
|
||||
const Divider(
|
||||
height: 0.5,
|
||||
thickness: 0.5,
|
||||
),
|
||||
Expanded(
|
||||
child: BlocBuilder<SortEditorBloc, SortEditorState>(
|
||||
builder: (context, state) {
|
||||
final fields = state.allFields
|
||||
.where(
|
||||
(field) =>
|
||||
field.canCreateSort ||
|
||||
sortInfo != null && sortInfo!.fieldId == field.id,
|
||||
)
|
||||
.toList();
|
||||
return ListView.builder(
|
||||
itemCount: fields.length,
|
||||
itemBuilder: (context, index) {
|
||||
final fieldInfo = fields[index];
|
||||
final isSelected = sortInfo == null
|
||||
? context
|
||||
.watch<MobileSortEditorCubit>()
|
||||
.state
|
||||
.newSortFieldId ==
|
||||
fieldInfo.id
|
||||
: sortInfo!.fieldId == fieldInfo.id;
|
||||
return FlowyOptionTile.checkbox(
|
||||
text: fieldInfo.field.name,
|
||||
isSelected: isSelected,
|
||||
showTopBorder: false,
|
||||
onTap: () {
|
||||
if (!isSelected) {
|
||||
_changeFieldId(context, fieldInfo.id);
|
||||
}
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
void _changeCondition(BuildContext context, SortConditionPB newCondition) {
|
||||
if (sortInfo == null) {
|
||||
context.read<MobileSortEditorCubit>().changeSortCondition(newCondition);
|
||||
} else {
|
||||
context.read<SortEditorBloc>().add(
|
||||
SortEditorEvent.editSort(
|
||||
sortInfo!.sortId,
|
||||
null,
|
||||
newCondition,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void _changeFieldId(BuildContext context, String newFieldId) {
|
||||
if (sortInfo == null) {
|
||||
context.read<MobileSortEditorCubit>().changeFieldId(newFieldId);
|
||||
} else {
|
||||
context.read<SortEditorBloc>().add(
|
||||
SortEditorEvent.editSort(
|
||||
sortInfo!.sortId,
|
||||
newFieldId,
|
||||
null,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,77 @@
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
part 'database_sort_bottom_sheet_cubit.freezed.dart';
|
||||
|
||||
class MobileSortEditorCubit extends Cubit<MobileSortEditorState> {
|
||||
MobileSortEditorCubit({
|
||||
required this.pageController,
|
||||
}) : super(MobileSortEditorState.initial());
|
||||
|
||||
final PageController pageController;
|
||||
|
||||
void returnToOverview() {
|
||||
_animateToPage(0);
|
||||
emit(MobileSortEditorState.initial());
|
||||
}
|
||||
|
||||
void startCreatingSort() {
|
||||
_animateToPage(1);
|
||||
emit(
|
||||
state.copyWith(
|
||||
showBackButton: true,
|
||||
isCreatingNewSort: true,
|
||||
newSortCondition: SortConditionPB.Ascending,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void startEditingSort(String sortId) {
|
||||
_animateToPage(1);
|
||||
emit(
|
||||
state.copyWith(
|
||||
showBackButton: true,
|
||||
editingSortId: sortId,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// only used when creating a new sort
|
||||
void changeFieldId(String fieldId) {
|
||||
emit(state.copyWith(newSortFieldId: fieldId));
|
||||
}
|
||||
|
||||
/// only used when creating a new sort
|
||||
void changeSortCondition(SortConditionPB condition) {
|
||||
emit(state.copyWith(newSortCondition: condition));
|
||||
}
|
||||
|
||||
Future<void> _animateToPage(int page) async {
|
||||
return pageController.animateToPage(
|
||||
page,
|
||||
duration: const Duration(milliseconds: 150),
|
||||
curve: Curves.easeOut,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@freezed
|
||||
class MobileSortEditorState with _$MobileSortEditorState {
|
||||
factory MobileSortEditorState({
|
||||
required bool showBackButton,
|
||||
required String? editingSortId,
|
||||
required bool isCreatingNewSort,
|
||||
required String? newSortFieldId,
|
||||
required SortConditionPB? newSortCondition,
|
||||
}) = _MobileSortEditorState;
|
||||
|
||||
factory MobileSortEditorState.initial() => MobileSortEditorState(
|
||||
showBackButton: false,
|
||||
editingSortId: null,
|
||||
isCreatingNewSort: false,
|
||||
newSortFieldId: null,
|
||||
newSortCondition: null,
|
||||
);
|
||||
}
|
@ -218,7 +218,7 @@ class FlowyOptionTile extends StatelessWidget {
|
||||
|
||||
final padding = EdgeInsets.symmetric(
|
||||
horizontal: leading == null ? 0.0 : 12.0,
|
||||
vertical: 16.0,
|
||||
vertical: 14.0,
|
||||
);
|
||||
|
||||
return Expanded(
|
||||
|
@ -3,7 +3,6 @@ import 'package:dartz/dartz.dart';
|
||||
import 'package:appflowy_backend/dispatch/dispatch.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/setting_entities.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/sort_entities.pb.dart';
|
||||
|
||||
@ -24,16 +23,15 @@ class SortBackendService {
|
||||
}
|
||||
|
||||
Future<Either<Unit, FlowyError>> updateSort({
|
||||
required String fieldId,
|
||||
required String sortId,
|
||||
required FieldType fieldType,
|
||||
required String fieldId,
|
||||
required SortConditionPB condition,
|
||||
}) {
|
||||
final insertSortPayload = UpdateSortPayloadPB.create()
|
||||
..fieldId = fieldId
|
||||
..viewId = viewId
|
||||
..condition = condition
|
||||
..sortId = sortId;
|
||||
..sortId = sortId
|
||||
..fieldId = fieldId
|
||||
..condition = condition;
|
||||
|
||||
final payload = DatabaseSettingChangesetPB.create()
|
||||
..viewId = viewId
|
||||
@ -51,7 +49,6 @@ class SortBackendService {
|
||||
|
||||
Future<Either<Unit, FlowyError>> insertSort({
|
||||
required String fieldId,
|
||||
required FieldType fieldType,
|
||||
required SortConditionPB condition,
|
||||
}) {
|
||||
final insertSortPayload = UpdateSortPayloadPB.create()
|
||||
@ -90,7 +87,6 @@ class SortBackendService {
|
||||
Future<Either<Unit, FlowyError>> deleteSort({
|
||||
required String fieldId,
|
||||
required String sortId,
|
||||
required FieldType fieldType,
|
||||
}) {
|
||||
final deleteSortPayload = DeleteSortPayloadPB.create()
|
||||
..sortId = sortId
|
||||
|
@ -91,7 +91,6 @@ class CreateSortBloc extends Bloc<CreateSortEvent, CreateSortState> {
|
||||
Future<Either<Unit, FlowyError>> _createDefaultSort(FieldInfo field) async {
|
||||
final result = await _sortBackendSvc.insertSort(
|
||||
fieldId: field.id,
|
||||
fieldType: field.fieldType,
|
||||
condition: SortConditionPB.Ascending,
|
||||
);
|
||||
|
||||
|
@ -7,6 +7,7 @@ import 'package:appflowy/plugins/database/grid/presentation/widgets/sort/sort_in
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/sort_entities.pbenum.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/sort_entities.pbserver.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
@ -20,14 +21,21 @@ class SortEditorBloc extends Bloc<SortEditorEvent, SortEditorState> {
|
||||
required this.fieldController,
|
||||
required List<SortInfo> sortInfos,
|
||||
}) : _sortBackendSvc = SortBackendService(viewId: viewId),
|
||||
super(SortEditorState.initial(sortInfos, fieldController.fieldInfos)) {
|
||||
super(
|
||||
SortEditorState.initial(
|
||||
sortInfos,
|
||||
fieldController.fieldInfos,
|
||||
),
|
||||
) {
|
||||
_dispatch();
|
||||
}
|
||||
|
||||
final String viewId;
|
||||
final SortBackendService _sortBackendSvc;
|
||||
final FieldController fieldController;
|
||||
|
||||
void Function(List<FieldInfo>)? _onFieldFn;
|
||||
void Function(List<SortInfo>)? _onSortsFn;
|
||||
|
||||
void _dispatch() {
|
||||
on<SortEditorEvent>(
|
||||
@ -37,25 +45,38 @@ class SortEditorBloc extends Bloc<SortEditorEvent, SortEditorState> {
|
||||
_startListening();
|
||||
},
|
||||
didReceiveFields: (List<FieldInfo> fields) {
|
||||
final List<FieldInfo> allFields = List.from(fields);
|
||||
final List<FieldInfo> creatableFields = List.from(fields);
|
||||
creatableFields.retainWhere((field) => field.canCreateSort);
|
||||
emit(
|
||||
state.copyWith(
|
||||
allFields: allFields,
|
||||
creatableFields: creatableFields,
|
||||
allFields: fields,
|
||||
creatableFields: getCreatableSorts(fields),
|
||||
),
|
||||
);
|
||||
},
|
||||
setCondition: (SortInfo sortInfo, SortConditionPB condition) async {
|
||||
final result = await _sortBackendSvc.updateSort(
|
||||
fieldId: sortInfo.fieldInfo.id,
|
||||
sortId: sortInfo.sortId,
|
||||
fieldType: sortInfo.fieldInfo.fieldType,
|
||||
createSort: (String fieldId, SortConditionPB condition) async {
|
||||
final result = await _sortBackendSvc.insertSort(
|
||||
fieldId: fieldId,
|
||||
condition: condition,
|
||||
);
|
||||
result.fold((l) => {}, (err) => Log.error(err));
|
||||
},
|
||||
editSort: (
|
||||
String sortId,
|
||||
String? fieldId,
|
||||
SortConditionPB? condition,
|
||||
) async {
|
||||
final sortInfo = state.sortInfos
|
||||
.firstWhereOrNull((element) => element.sortId == sortId);
|
||||
if (sortInfo == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final result = await _sortBackendSvc.updateSort(
|
||||
sortId: sortId,
|
||||
fieldId: fieldId ?? sortInfo.fieldId,
|
||||
condition: condition ?? sortInfo.sortPB.condition,
|
||||
);
|
||||
result.fold((l) => {}, (err) => Log.error(err));
|
||||
},
|
||||
deleteAllSorts: () async {
|
||||
final result = await _sortBackendSvc.deleteAllSorts();
|
||||
result.fold((l) => {}, (err) => Log.error(err));
|
||||
@ -67,7 +88,6 @@ class SortEditorBloc extends Bloc<SortEditorEvent, SortEditorState> {
|
||||
final result = await _sortBackendSvc.deleteSort(
|
||||
fieldId: sortInfo.fieldInfo.id,
|
||||
sortId: sortInfo.sortId,
|
||||
fieldType: sortInfo.fieldInfo.fieldType,
|
||||
);
|
||||
result.fold((l) => null, (err) => Log.error(err));
|
||||
},
|
||||
@ -97,22 +117,25 @@ class SortEditorBloc extends Bloc<SortEditorEvent, SortEditorState> {
|
||||
_onFieldFn = (fields) {
|
||||
add(SortEditorEvent.didReceiveFields(List.from(fields)));
|
||||
};
|
||||
_onSortsFn = (sorts) {
|
||||
add(SortEditorEvent.didReceiveSorts(sorts));
|
||||
};
|
||||
|
||||
fieldController.addListener(
|
||||
listenWhen: () => !isClosed,
|
||||
onReceiveFields: _onFieldFn,
|
||||
onSorts: (sorts) {
|
||||
add(SortEditorEvent.didReceiveSorts(sorts));
|
||||
},
|
||||
onSorts: _onSortsFn,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> close() async {
|
||||
if (_onFieldFn != null) {
|
||||
fieldController.removeListener(onFieldsListener: _onFieldFn);
|
||||
_onFieldFn = null;
|
||||
}
|
||||
fieldController.removeListener(
|
||||
onFieldsListener: _onFieldFn,
|
||||
onSortsListener: _onSortsFn,
|
||||
);
|
||||
_onFieldFn = null;
|
||||
_onSortsFn = null;
|
||||
return super.close();
|
||||
}
|
||||
}
|
||||
@ -124,10 +147,15 @@ class SortEditorEvent with _$SortEditorEvent {
|
||||
_DidReceiveFields;
|
||||
const factory SortEditorEvent.didReceiveSorts(List<SortInfo> sortInfos) =
|
||||
_DidReceiveSorts;
|
||||
const factory SortEditorEvent.setCondition(
|
||||
SortInfo sortInfo,
|
||||
const factory SortEditorEvent.createSort(
|
||||
String fieldId,
|
||||
SortConditionPB condition,
|
||||
) = _SetCondition;
|
||||
) = _CreateSort;
|
||||
const factory SortEditorEvent.editSort(
|
||||
String sortId,
|
||||
String? fieldId,
|
||||
SortConditionPB? condition,
|
||||
) = _EditSort;
|
||||
const factory SortEditorEvent.deleteSort(SortInfo sortInfo) = _DeleteSort;
|
||||
const factory SortEditorEvent.deleteAllSorts() = _DeleteAllSorts;
|
||||
const factory SortEditorEvent.reorderSort(int oldIndex, int newIndex) =
|
||||
|
@ -299,9 +299,13 @@ class _DatabaseSortItemOrderButtonState
|
||||
popupBuilder: (BuildContext popoverContext) {
|
||||
return OrderPanel(
|
||||
onCondition: (condition) {
|
||||
context
|
||||
.read<SortEditorBloc>()
|
||||
.add(SortEditorEvent.setCondition(widget.sortInfo, condition));
|
||||
context.read<SortEditorBloc>().add(
|
||||
SortEditorEvent.editSort(
|
||||
widget.sortInfo.sortId,
|
||||
null,
|
||||
condition,
|
||||
),
|
||||
);
|
||||
popoverController.close();
|
||||
},
|
||||
);
|
||||
|
@ -2,12 +2,15 @@ import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
|
||||
import 'package:appflowy/mobile/presentation/database/view/database_field_list.dart';
|
||||
import 'package:appflowy/mobile/presentation/database/view/database_sort_bottom_sheet.dart';
|
||||
import 'package:appflowy/plugins/database/application/database_controller.dart';
|
||||
import 'package:appflowy/plugins/database/grid/application/filter/filter_menu_bloc.dart';
|
||||
import 'package:appflowy/plugins/database/grid/application/sort/sort_editor_bloc.dart';
|
||||
import 'package:appflowy/plugins/database/grid/application/sort/sort_menu_bloc.dart';
|
||||
import 'package:appflowy/plugins/database/grid/presentation/grid_page.dart';
|
||||
import 'package:appflowy/workspace/application/view/view_bloc.dart';
|
||||
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';
|
||||
|
||||
@ -56,24 +59,25 @@ class MobileDatabaseControls extends StatelessWidget {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return _DatabaseControlButton(
|
||||
icon: FlowySvgs.m_field_hide_s,
|
||||
onTap: () => showTransitionMobileBottomSheet(
|
||||
context,
|
||||
showHeader: true,
|
||||
showBackButton: true,
|
||||
title: LocaleKeys.grid_settings_properties.tr(),
|
||||
showDivider: true,
|
||||
builder: (_) {
|
||||
return BlocProvider.value(
|
||||
value: context.read<ViewBloc>(),
|
||||
child: MobileDatabaseFieldList(
|
||||
databaseController: controller,
|
||||
canCreate: false,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
return SeparatedRow(
|
||||
separatorBuilder: () => const HSpace(8.0),
|
||||
children: [
|
||||
_DatabaseControlButton(
|
||||
icon: FlowySvgs.sort_ascending_s,
|
||||
count: context.watch<SortMenuBloc>().state.sortInfos.length,
|
||||
onTap: () => _showEditSortPanelFromToolbar(
|
||||
context,
|
||||
controller,
|
||||
),
|
||||
),
|
||||
_DatabaseControlButton(
|
||||
icon: FlowySvgs.m_field_hide_s,
|
||||
onTap: () => _showDatabaseFieldListFromToolbar(
|
||||
context,
|
||||
controller,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
@ -86,24 +90,85 @@ class _DatabaseControlButton extends StatelessWidget {
|
||||
const _DatabaseControlButton({
|
||||
required this.onTap,
|
||||
required this.icon,
|
||||
this.count = 0,
|
||||
});
|
||||
|
||||
final VoidCallback onTap;
|
||||
final FlowySvgData icon;
|
||||
final int count;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox.square(
|
||||
dimension: 36,
|
||||
child: IconButton(
|
||||
splashRadius: 18,
|
||||
padding: EdgeInsets.zero,
|
||||
onPressed: onTap,
|
||||
icon: FlowySvg(
|
||||
icon,
|
||||
size: const Size.square(20),
|
||||
),
|
||||
return InkWell(
|
||||
onTap: onTap,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(5.0),
|
||||
child: count == 0
|
||||
? FlowySvg(
|
||||
icon,
|
||||
size: const Size.square(20),
|
||||
)
|
||||
: Row(
|
||||
children: [
|
||||
FlowySvg(
|
||||
icon,
|
||||
size: const Size.square(20),
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
const HSpace(2.0),
|
||||
FlowyText.medium(
|
||||
count.toString(),
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void _showDatabaseFieldListFromToolbar(
|
||||
BuildContext context,
|
||||
DatabaseController databaseController,
|
||||
) {
|
||||
showTransitionMobileBottomSheet(
|
||||
context,
|
||||
showHeader: true,
|
||||
showBackButton: true,
|
||||
title: LocaleKeys.grid_settings_properties.tr(),
|
||||
showDivider: true,
|
||||
builder: (_) {
|
||||
return BlocProvider.value(
|
||||
value: context.read<ViewBloc>(),
|
||||
child: MobileDatabaseFieldList(
|
||||
databaseController: databaseController,
|
||||
canCreate: false,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void _showEditSortPanelFromToolbar(
|
||||
BuildContext context,
|
||||
DatabaseController databaseController,
|
||||
) {
|
||||
showMobileBottomSheet(
|
||||
context,
|
||||
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||
showDragHandle: true,
|
||||
showDivider: false,
|
||||
useSafeArea: false,
|
||||
builder: (_) {
|
||||
return BlocProvider(
|
||||
create: (_) => SortEditorBloc(
|
||||
viewId: databaseController.viewId,
|
||||
fieldController: databaseController.fieldController,
|
||||
sortInfos: databaseController.fieldController.sortInfos,
|
||||
)..add(const SortEditorEvent.initial()),
|
||||
child: const MobileSortEditor(),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
@ -0,0 +1,3 @@
|
||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M11.4958 4.83754C11.268 4.60973 10.8986 4.60973 10.6708 4.83754L6.99996 8.5084L3.3291 4.83754C3.1013 4.60973 2.73195 4.60973 2.50415 4.83754C2.27634 5.06535 2.27634 5.43469 2.50415 5.6625L6.58748 9.74583C6.69688 9.85523 6.84525 9.91669 6.99996 9.91669C7.15467 9.91669 7.30304 9.85523 7.41244 9.74583L11.4958 5.6625C11.7236 5.43469 11.7236 5.06535 11.4958 4.83754Z" fill="#1F2329"/>
|
||||
</svg>
|
After Width: | Height: | Size: 494 B |
@ -621,8 +621,11 @@
|
||||
"sort": {
|
||||
"ascending": "Ascending",
|
||||
"descending": "Descending",
|
||||
"by": "By",
|
||||
"empty": "No active sorts",
|
||||
"cannotFindCreatableField": "Cannot find a suitable field to sort by",
|
||||
"deleteAllSorts": "Delete all sorts",
|
||||
"addSort": "Add sort"
|
||||
"addSort": "Add new sort"
|
||||
},
|
||||
"row": {
|
||||
"duplicate": "Duplicate",
|
||||
|
Loading…
Reference in New Issue
Block a user