feat: mobile sort editor (#4714)

This commit is contained in:
Richard Shiue 2024-02-23 20:47:19 +08:00 committed by GitHub
parent acd75befbc
commit c0796e8ae5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 835 additions and 74 deletions

View File

@ -8,14 +8,17 @@ class AppBarBackButton extends StatelessWidget {
const AppBarBackButton({ const AppBarBackButton({
super.key, super.key,
this.onTap, this.onTap,
this.padding,
}); });
final VoidCallback? onTap; final VoidCallback? onTap;
final EdgeInsetsGeometry? padding;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return AppBarButton( return AppBarButton(
onTap: onTap ?? () => Navigator.pop(context), onTap: onTap ?? () => Navigator.pop(context),
padding: padding,
child: const FlowySvg( child: const FlowySvg(
FlowySvgs.m_app_bar_back_s, FlowySvgs.m_app_bar_back_s,
), ),
@ -73,8 +76,8 @@ class AppBarDoneButton extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return AppBarButton( return AppBarButton(
isActionButton: true,
onTap: onTap, onTap: onTap,
padding: const EdgeInsets.fromLTRB(12, 12, 8, 12),
child: FlowyText( child: FlowyText(
LocaleKeys.button_Done.tr(), LocaleKeys.button_Done.tr(),
color: Theme.of(context).colorScheme.primary, 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 { class AppBarFilledDoneButton extends StatelessWidget {
const AppBarFilledDoneButton({super.key, required this.onTap}); const AppBarFilledDoneButton({super.key, required this.onTap});
@ -129,7 +165,7 @@ class AppBarMoreButton extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return AppBarButton( return AppBarButton(
isActionButton: true, padding: const EdgeInsets.fromLTRB(12, 12, 8, 12),
onTap: () => onTap(context), onTap: () => onTap(context),
child: const FlowySvg(FlowySvgs.three_dots_s), child: const FlowySvg(FlowySvgs.three_dots_s),
); );
@ -139,14 +175,14 @@ class AppBarMoreButton extends StatelessWidget {
class AppBarButton extends StatelessWidget { class AppBarButton extends StatelessWidget {
const AppBarButton({ const AppBarButton({
super.key, super.key,
this.isActionButton = false,
required this.onTap, required this.onTap,
required this.child, required this.child,
this.padding,
}); });
final VoidCallback onTap; final VoidCallback onTap;
final Widget child; final Widget child;
final bool isActionButton; final EdgeInsetsGeometry? padding;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -154,12 +190,7 @@ class AppBarButton extends StatelessWidget {
behavior: HitTestBehavior.opaque, behavior: HitTestBehavior.opaque,
onTap: onTap, onTap: onTap,
child: Padding( child: Padding(
padding: EdgeInsets.only( padding: padding ?? const EdgeInsets.all(12),
top: 12.0,
bottom: 12.0,
left: 12.0,
right: isActionButton ? 12.0 : 8.0,
),
child: child, child: child,
), ),
); );

View File

@ -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,
),
);
}
}
}

View File

@ -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,
);
}

View File

@ -218,7 +218,7 @@ class FlowyOptionTile extends StatelessWidget {
final padding = EdgeInsets.symmetric( final padding = EdgeInsets.symmetric(
horizontal: leading == null ? 0.0 : 12.0, horizontal: leading == null ? 0.0 : 12.0,
vertical: 16.0, vertical: 14.0,
); );
return Expanded( return Expanded(

View File

@ -3,7 +3,6 @@ import 'package:dartz/dartz.dart';
import 'package:appflowy_backend/dispatch/dispatch.dart'; import 'package:appflowy_backend/dispatch/dispatch.dart';
import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.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/setting_entities.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/sort_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({ Future<Either<Unit, FlowyError>> updateSort({
required String fieldId,
required String sortId, required String sortId,
required FieldType fieldType, required String fieldId,
required SortConditionPB condition, required SortConditionPB condition,
}) { }) {
final insertSortPayload = UpdateSortPayloadPB.create() final insertSortPayload = UpdateSortPayloadPB.create()
..fieldId = fieldId
..viewId = viewId ..viewId = viewId
..condition = condition ..sortId = sortId
..sortId = sortId; ..fieldId = fieldId
..condition = condition;
final payload = DatabaseSettingChangesetPB.create() final payload = DatabaseSettingChangesetPB.create()
..viewId = viewId ..viewId = viewId
@ -51,7 +49,6 @@ class SortBackendService {
Future<Either<Unit, FlowyError>> insertSort({ Future<Either<Unit, FlowyError>> insertSort({
required String fieldId, required String fieldId,
required FieldType fieldType,
required SortConditionPB condition, required SortConditionPB condition,
}) { }) {
final insertSortPayload = UpdateSortPayloadPB.create() final insertSortPayload = UpdateSortPayloadPB.create()
@ -90,7 +87,6 @@ class SortBackendService {
Future<Either<Unit, FlowyError>> deleteSort({ Future<Either<Unit, FlowyError>> deleteSort({
required String fieldId, required String fieldId,
required String sortId, required String sortId,
required FieldType fieldType,
}) { }) {
final deleteSortPayload = DeleteSortPayloadPB.create() final deleteSortPayload = DeleteSortPayloadPB.create()
..sortId = sortId ..sortId = sortId

View File

@ -91,7 +91,6 @@ class CreateSortBloc extends Bloc<CreateSortEvent, CreateSortState> {
Future<Either<Unit, FlowyError>> _createDefaultSort(FieldInfo field) async { Future<Either<Unit, FlowyError>> _createDefaultSort(FieldInfo field) async {
final result = await _sortBackendSvc.insertSort( final result = await _sortBackendSvc.insertSort(
fieldId: field.id, fieldId: field.id,
fieldType: field.fieldType,
condition: SortConditionPB.Ascending, condition: SortConditionPB.Ascending,
); );

View File

@ -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/log.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/sort_entities.pbenum.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:appflowy_backend/protobuf/flowy-database2/sort_entities.pbserver.dart';
import 'package:collection/collection.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:freezed_annotation/freezed_annotation.dart';
@ -20,14 +21,21 @@ class SortEditorBloc extends Bloc<SortEditorEvent, SortEditorState> {
required this.fieldController, required this.fieldController,
required List<SortInfo> sortInfos, required List<SortInfo> sortInfos,
}) : _sortBackendSvc = SortBackendService(viewId: viewId), }) : _sortBackendSvc = SortBackendService(viewId: viewId),
super(SortEditorState.initial(sortInfos, fieldController.fieldInfos)) { super(
SortEditorState.initial(
sortInfos,
fieldController.fieldInfos,
),
) {
_dispatch(); _dispatch();
} }
final String viewId; final String viewId;
final SortBackendService _sortBackendSvc; final SortBackendService _sortBackendSvc;
final FieldController fieldController; final FieldController fieldController;
void Function(List<FieldInfo>)? _onFieldFn; void Function(List<FieldInfo>)? _onFieldFn;
void Function(List<SortInfo>)? _onSortsFn;
void _dispatch() { void _dispatch() {
on<SortEditorEvent>( on<SortEditorEvent>(
@ -37,25 +45,38 @@ class SortEditorBloc extends Bloc<SortEditorEvent, SortEditorState> {
_startListening(); _startListening();
}, },
didReceiveFields: (List<FieldInfo> fields) { didReceiveFields: (List<FieldInfo> fields) {
final List<FieldInfo> allFields = List.from(fields);
final List<FieldInfo> creatableFields = List.from(fields);
creatableFields.retainWhere((field) => field.canCreateSort);
emit( emit(
state.copyWith( state.copyWith(
allFields: allFields, allFields: fields,
creatableFields: creatableFields, creatableFields: getCreatableSorts(fields),
), ),
); );
}, },
setCondition: (SortInfo sortInfo, SortConditionPB condition) async { createSort: (String fieldId, SortConditionPB condition) async {
final result = await _sortBackendSvc.updateSort( final result = await _sortBackendSvc.insertSort(
fieldId: sortInfo.fieldInfo.id, fieldId: fieldId,
sortId: sortInfo.sortId,
fieldType: sortInfo.fieldInfo.fieldType,
condition: condition, condition: condition,
); );
result.fold((l) => {}, (err) => Log.error(err)); 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 { deleteAllSorts: () async {
final result = await _sortBackendSvc.deleteAllSorts(); final result = await _sortBackendSvc.deleteAllSorts();
result.fold((l) => {}, (err) => Log.error(err)); result.fold((l) => {}, (err) => Log.error(err));
@ -67,7 +88,6 @@ class SortEditorBloc extends Bloc<SortEditorEvent, SortEditorState> {
final result = await _sortBackendSvc.deleteSort( final result = await _sortBackendSvc.deleteSort(
fieldId: sortInfo.fieldInfo.id, fieldId: sortInfo.fieldInfo.id,
sortId: sortInfo.sortId, sortId: sortInfo.sortId,
fieldType: sortInfo.fieldInfo.fieldType,
); );
result.fold((l) => null, (err) => Log.error(err)); result.fold((l) => null, (err) => Log.error(err));
}, },
@ -97,22 +117,25 @@ class SortEditorBloc extends Bloc<SortEditorEvent, SortEditorState> {
_onFieldFn = (fields) { _onFieldFn = (fields) {
add(SortEditorEvent.didReceiveFields(List.from(fields))); add(SortEditorEvent.didReceiveFields(List.from(fields)));
}; };
_onSortsFn = (sorts) {
add(SortEditorEvent.didReceiveSorts(sorts));
};
fieldController.addListener( fieldController.addListener(
listenWhen: () => !isClosed, listenWhen: () => !isClosed,
onReceiveFields: _onFieldFn, onReceiveFields: _onFieldFn,
onSorts: (sorts) { onSorts: _onSortsFn,
add(SortEditorEvent.didReceiveSorts(sorts));
},
); );
} }
@override @override
Future<void> close() async { Future<void> close() async {
if (_onFieldFn != null) { fieldController.removeListener(
fieldController.removeListener(onFieldsListener: _onFieldFn); onFieldsListener: _onFieldFn,
_onFieldFn = null; onSortsListener: _onSortsFn,
} );
_onFieldFn = null;
_onSortsFn = null;
return super.close(); return super.close();
} }
} }
@ -124,10 +147,15 @@ class SortEditorEvent with _$SortEditorEvent {
_DidReceiveFields; _DidReceiveFields;
const factory SortEditorEvent.didReceiveSorts(List<SortInfo> sortInfos) = const factory SortEditorEvent.didReceiveSorts(List<SortInfo> sortInfos) =
_DidReceiveSorts; _DidReceiveSorts;
const factory SortEditorEvent.setCondition( const factory SortEditorEvent.createSort(
SortInfo sortInfo, String fieldId,
SortConditionPB condition, 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.deleteSort(SortInfo sortInfo) = _DeleteSort;
const factory SortEditorEvent.deleteAllSorts() = _DeleteAllSorts; const factory SortEditorEvent.deleteAllSorts() = _DeleteAllSorts;
const factory SortEditorEvent.reorderSort(int oldIndex, int newIndex) = const factory SortEditorEvent.reorderSort(int oldIndex, int newIndex) =

View File

@ -299,9 +299,13 @@ class _DatabaseSortItemOrderButtonState
popupBuilder: (BuildContext popoverContext) { popupBuilder: (BuildContext popoverContext) {
return OrderPanel( return OrderPanel(
onCondition: (condition) { onCondition: (condition) {
context context.read<SortEditorBloc>().add(
.read<SortEditorBloc>() SortEditorEvent.editSort(
.add(SortEditorEvent.setCondition(widget.sortInfo, condition)); widget.sortInfo.sortId,
null,
condition,
),
);
popoverController.close(); popoverController.close();
}, },
); );

View File

@ -2,12 +2,15 @@ import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.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_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/application/database_controller.dart';
import 'package:appflowy/plugins/database/grid/application/filter/filter_menu_bloc.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/application/sort/sort_menu_bloc.dart';
import 'package:appflowy/plugins/database/grid/presentation/grid_page.dart'; import 'package:appflowy/plugins/database/grid/presentation/grid_page.dart';
import 'package:appflowy/workspace/application/view/view_bloc.dart'; import 'package:appflowy/workspace/application/view/view_bloc.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
@ -56,24 +59,25 @@ class MobileDatabaseControls extends StatelessWidget {
return const SizedBox.shrink(); return const SizedBox.shrink();
} }
return _DatabaseControlButton( return SeparatedRow(
icon: FlowySvgs.m_field_hide_s, separatorBuilder: () => const HSpace(8.0),
onTap: () => showTransitionMobileBottomSheet( children: [
context, _DatabaseControlButton(
showHeader: true, icon: FlowySvgs.sort_ascending_s,
showBackButton: true, count: context.watch<SortMenuBloc>().state.sortInfos.length,
title: LocaleKeys.grid_settings_properties.tr(), onTap: () => _showEditSortPanelFromToolbar(
showDivider: true, context,
builder: (_) { controller,
return BlocProvider.value( ),
value: context.read<ViewBloc>(), ),
child: MobileDatabaseFieldList( _DatabaseControlButton(
databaseController: controller, icon: FlowySvgs.m_field_hide_s,
canCreate: false, onTap: () => _showDatabaseFieldListFromToolbar(
), context,
); controller,
}, ),
), ),
],
); );
}, },
), ),
@ -86,24 +90,85 @@ class _DatabaseControlButton extends StatelessWidget {
const _DatabaseControlButton({ const _DatabaseControlButton({
required this.onTap, required this.onTap,
required this.icon, required this.icon,
this.count = 0,
}); });
final VoidCallback onTap; final VoidCallback onTap;
final FlowySvgData icon; final FlowySvgData icon;
final int count;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return SizedBox.square( return InkWell(
dimension: 36, onTap: onTap,
child: IconButton( borderRadius: BorderRadius.circular(10),
splashRadius: 18, child: Padding(
padding: EdgeInsets.zero, padding: const EdgeInsets.all(5.0),
onPressed: onTap, child: count == 0
icon: FlowySvg( ? FlowySvg(
icon, icon,
size: const Size.square(20), 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(),
);
},
);
}

View File

@ -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

View File

@ -621,8 +621,11 @@
"sort": { "sort": {
"ascending": "Ascending", "ascending": "Ascending",
"descending": "Descending", "descending": "Descending",
"by": "By",
"empty": "No active sorts",
"cannotFindCreatableField": "Cannot find a suitable field to sort by",
"deleteAllSorts": "Delete all sorts", "deleteAllSorts": "Delete all sorts",
"addSort": "Add sort" "addSort": "Add new sort"
}, },
"row": { "row": {
"duplicate": "Duplicate", "duplicate": "Duplicate",