mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2025-09-06 15:23:48 +00:00
feat(flutter_desktop): date filter (#6288)
* feat: wip enabling date filter * fix: update date picker selected range on change * fix: save in utc, load in local date time * test: added date filter test * fix: don't include time in date picker including time makes filtering complex without much gain. when the condition is "is" we should also note the exact time which most of the cases the time is not intended by user only the day. * fix: flutter analyze
This commit is contained in:
parent
da7c993fd6
commit
b7c598ea56
@ -1,6 +1,6 @@
|
||||
import 'package:appflowy/plugins/database/grid/presentation/widgets/filter/choicechip/checkbox.dart';
|
||||
import 'package:appflowy/plugins/database/grid/presentation/widgets/filter/choicechip/text.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
@ -138,5 +138,27 @@ void main() {
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
});
|
||||
|
||||
testWidgets('add date filter', (tester) async {
|
||||
await tester.openTestDatabase(v020GridFileName);
|
||||
|
||||
// create a filter
|
||||
await tester.tapDatabaseFilterButton();
|
||||
await tester.tapCreateFilterByFieldType(FieldType.DateTime, 'date');
|
||||
|
||||
// By default, the condition of date filter is current day and time
|
||||
tester.assertNumberOfRowsInGridPage(0);
|
||||
|
||||
await tester.tapFilterButtonInGrid('date');
|
||||
await tester.tapDateFilterButtonInGrid();
|
||||
await tester.tapDateFilterCondition(DateFilterConditionPB.DateBefore);
|
||||
tester.assertNumberOfRowsInGridPage(7);
|
||||
|
||||
await tester.tapDateFilterButtonInGrid();
|
||||
await tester.tapDateFilterCondition(DateFilterConditionPB.DateIsEmpty);
|
||||
tester.assertNumberOfRowsInGridPage(3);
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
@ -24,6 +24,7 @@ import 'package:appflowy/plugins/database/grid/presentation/widgets/filter/choic
|
||||
import 'package:appflowy/plugins/database/grid/presentation/widgets/filter/choicechip/select_option/option_list.dart';
|
||||
import 'package:appflowy/plugins/database/grid/presentation/widgets/filter/choicechip/select_option/select_option.dart';
|
||||
import 'package:appflowy/plugins/database/grid/presentation/widgets/filter/choicechip/text.dart';
|
||||
import 'package:appflowy/plugins/database/grid/presentation/widgets/filter/choicechip/date.dart';
|
||||
import 'package:appflowy/plugins/database/grid/presentation/widgets/filter/create_filter_list.dart';
|
||||
import 'package:appflowy/plugins/database/grid/presentation/widgets/filter/disclosure_button.dart';
|
||||
import 'package:appflowy/plugins/database/grid/presentation/widgets/filter/filter_menu_item.dart';
|
||||
@ -1096,6 +1097,10 @@ extension AppFlowyDatabaseTest on WidgetTester {
|
||||
await tapButton(find.byType(ChecklistFilterConditionList));
|
||||
}
|
||||
|
||||
Future<void> tapDateFilterButtonInGrid() async {
|
||||
await tapButton(find.byType(DateFilterConditionPBList));
|
||||
}
|
||||
|
||||
/// The [SelectOptionFilterList] must show up first.
|
||||
Future<void> tapOptionFilterWithName(String name) async {
|
||||
final findCell = find.descendant(
|
||||
@ -1129,6 +1134,15 @@ extension AppFlowyDatabaseTest on WidgetTester {
|
||||
await tapButton(button);
|
||||
}
|
||||
|
||||
Future<void> tapDateFilterCondition(DateFilterConditionPB condition) async {
|
||||
final button = find.descendant(
|
||||
of: find.byType(HoverButton),
|
||||
matching: find.text(condition.filterName),
|
||||
);
|
||||
|
||||
await tapButton(button);
|
||||
}
|
||||
|
||||
/// Should call [tapDatabaseSettingButton] first.
|
||||
Future<void> tapViewPropertiesButton() async {
|
||||
final findSettingItem = find.byType(DatabaseSettingsList);
|
||||
|
@ -65,6 +65,7 @@ class FieldInfo with _$FieldInfo {
|
||||
case FieldType.Checklist:
|
||||
case FieldType.URL:
|
||||
case FieldType.Time:
|
||||
case FieldType.DateTime:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
|
@ -94,18 +94,11 @@ class FilterBackendService {
|
||||
required String fieldId,
|
||||
String? filterId,
|
||||
required DateFilterConditionPB condition,
|
||||
required FieldType fieldType,
|
||||
int? start,
|
||||
int? end,
|
||||
int? timestamp,
|
||||
}) {
|
||||
assert(
|
||||
fieldType == FieldType.DateTime ||
|
||||
fieldType == FieldType.LastEditedTime ||
|
||||
fieldType == FieldType.CreatedTime,
|
||||
);
|
||||
|
||||
final filter = DateFilterPB();
|
||||
final filter = DateFilterPB()..condition = condition;
|
||||
|
||||
if (timestamp != null) {
|
||||
filter.timestamp = $fixnum.Int64(timestamp);
|
||||
|
@ -0,0 +1,132 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:appflowy/plugins/database/domain/filter_listener.dart';
|
||||
import 'package:appflowy/plugins/database/domain/filter_service.dart';
|
||||
import 'package:appflowy/plugins/database/grid/presentation/widgets/filter/filter_info.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
part 'date_filter_editor_bloc.freezed.dart';
|
||||
|
||||
class DateFilterEditorBloc
|
||||
extends Bloc<DateFilterEditorEvent, DateFilterEditorState> {
|
||||
DateFilterEditorBloc({required this.filterInfo})
|
||||
: _filterBackendSvc = FilterBackendService(viewId: filterInfo.viewId),
|
||||
_listener = FilterListener(
|
||||
viewId: filterInfo.viewId,
|
||||
filterId: filterInfo.filter.id,
|
||||
),
|
||||
super(DateFilterEditorState.initial(filterInfo)) {
|
||||
_dispatch();
|
||||
_startListening();
|
||||
}
|
||||
|
||||
final FilterInfo filterInfo;
|
||||
final FilterBackendService _filterBackendSvc;
|
||||
final FilterListener _listener;
|
||||
|
||||
void _dispatch() {
|
||||
on<DateFilterEditorEvent>(
|
||||
(event, emit) async {
|
||||
event.when(
|
||||
didReceiveFilter: (filter) {
|
||||
final filterInfo = state.filterInfo.copyWith(filter: filter);
|
||||
emit(
|
||||
state.copyWith(
|
||||
filterInfo: filterInfo,
|
||||
filter: filterInfo.dateFilter()!,
|
||||
),
|
||||
);
|
||||
},
|
||||
updateCondition: (DateFilterConditionPB condition) {
|
||||
_filterBackendSvc.insertDateFilter(
|
||||
filterId: filterInfo.filter.id,
|
||||
fieldId: filterInfo.fieldInfo.id,
|
||||
condition: condition,
|
||||
start: state.filter.start.toInt(),
|
||||
end: state.filter.end.toInt(),
|
||||
timestamp: state.filter.timestamp.toInt(),
|
||||
);
|
||||
},
|
||||
updateDate: (date) {
|
||||
_filterBackendSvc.insertDateFilter(
|
||||
filterId: filterInfo.filter.id,
|
||||
fieldId: filterInfo.fieldInfo.id,
|
||||
condition: state.filter.condition,
|
||||
timestamp: date.millisecondsSinceEpoch ~/ 1000,
|
||||
);
|
||||
},
|
||||
updateRange: (start, end) {
|
||||
assert(start != null || end != null);
|
||||
_filterBackendSvc.insertDateFilter(
|
||||
filterId: filterInfo.filter.id,
|
||||
fieldId: filterInfo.fieldInfo.id,
|
||||
condition: state.filter.condition,
|
||||
start: start != null
|
||||
? start.millisecondsSinceEpoch ~/ 1000
|
||||
: state.filter.start.toInt(),
|
||||
end: end != null
|
||||
? end.millisecondsSinceEpoch ~/ 1000
|
||||
: state.filter.end.toInt(),
|
||||
);
|
||||
},
|
||||
delete: () {
|
||||
_filterBackendSvc.deleteFilter(
|
||||
fieldId: filterInfo.fieldInfo.id,
|
||||
filterId: filterInfo.filter.id,
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void _startListening() {
|
||||
_listener.start(
|
||||
onUpdated: (filter) {
|
||||
if (!isClosed) {
|
||||
add(DateFilterEditorEvent.didReceiveFilter(filter));
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> close() async {
|
||||
await _listener.stop();
|
||||
return super.close();
|
||||
}
|
||||
}
|
||||
|
||||
@freezed
|
||||
class DateFilterEditorEvent with _$DateFilterEditorEvent {
|
||||
const factory DateFilterEditorEvent.didReceiveFilter(FilterPB filter) =
|
||||
_DidReceiveFilter;
|
||||
const factory DateFilterEditorEvent.updateCondition(
|
||||
DateFilterConditionPB condition,
|
||||
) = _UpdateCondition;
|
||||
const factory DateFilterEditorEvent.updateDate(
|
||||
DateTime date,
|
||||
) = _UpdateDate;
|
||||
const factory DateFilterEditorEvent.updateRange({
|
||||
DateTime? start,
|
||||
DateTime? end,
|
||||
}) = _UpdateRange;
|
||||
const factory DateFilterEditorEvent.delete() = _Delete;
|
||||
}
|
||||
|
||||
@freezed
|
||||
class DateFilterEditorState with _$DateFilterEditorState {
|
||||
const factory DateFilterEditorState({
|
||||
required FilterInfo filterInfo,
|
||||
required DateFilterPB filter,
|
||||
}) = _DateFilterEditorState;
|
||||
|
||||
factory DateFilterEditorState.initial(FilterInfo filterInfo) {
|
||||
return DateFilterEditorState(
|
||||
filterInfo: filterInfo,
|
||||
filter: filterInfo.dateFilter()!,
|
||||
);
|
||||
}
|
||||
}
|
@ -103,7 +103,6 @@ class GridCreateFilterBloc
|
||||
fieldId: fieldId,
|
||||
condition: DateFilterConditionPB.DateIs,
|
||||
timestamp: timestamp,
|
||||
fieldType: field.fieldType,
|
||||
);
|
||||
case FieldType.MultiSelect:
|
||||
return _filterBackendSvc.insertSelectOptionFilter(
|
||||
|
@ -1,15 +1,304 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/database/grid/application/filter/date_filter_editor_bloc.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
|
||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/date_picker/appflowy_date_picker.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:appflowy/util/int64_extension.dart';
|
||||
|
||||
import '../condition_button.dart';
|
||||
import '../disclosure_button.dart';
|
||||
import '../filter_info.dart';
|
||||
|
||||
import 'choicechip.dart';
|
||||
|
||||
class DateFilterChoicechip extends StatelessWidget {
|
||||
const DateFilterChoicechip({required this.filterInfo, super.key});
|
||||
class DateFilterChoicechip extends StatefulWidget {
|
||||
const DateFilterChoicechip({
|
||||
super.key,
|
||||
required this.filterInfo,
|
||||
});
|
||||
|
||||
final FilterInfo filterInfo;
|
||||
|
||||
@override
|
||||
State<DateFilterChoicechip> createState() => _DateFilterChoicechipState();
|
||||
}
|
||||
|
||||
class _DateFilterChoicechipState extends State<DateFilterChoicechip> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ChoiceChipButton(filterInfo: filterInfo);
|
||||
return BlocProvider(
|
||||
create: (_) => DateFilterEditorBloc(filterInfo: widget.filterInfo),
|
||||
child: BlocBuilder<DateFilterEditorBloc, DateFilterEditorState>(
|
||||
builder: (context, state) {
|
||||
return AppFlowyPopover(
|
||||
constraints: BoxConstraints.loose(const Size(200, 120)),
|
||||
direction: PopoverDirection.bottomWithCenterAligned,
|
||||
popupBuilder: (_) {
|
||||
return BlocProvider.value(
|
||||
value: context.read<DateFilterEditorBloc>(),
|
||||
child: const DateFilterEditor(),
|
||||
);
|
||||
},
|
||||
child: ChoiceChipButton(filterInfo: state.filterInfo),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class DateFilterEditor extends StatefulWidget {
|
||||
const DateFilterEditor({super.key});
|
||||
|
||||
@override
|
||||
State<DateFilterEditor> createState() => _DateFilterEditorState();
|
||||
}
|
||||
|
||||
class _DateFilterEditorState extends State<DateFilterEditor> {
|
||||
final popoverMutex = PopoverMutex();
|
||||
final _popover = PopoverController();
|
||||
final _textEditingController = TextEditingController();
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
popoverMutex.dispose();
|
||||
_textEditingController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<DateFilterEditorBloc, DateFilterEditorState>(
|
||||
builder: (context, state) {
|
||||
final List<Widget> children = [
|
||||
_buildFilterPanel(context),
|
||||
if (state.filter.condition != DateFilterConditionPB.DateIsEmpty &&
|
||||
state.filter.condition !=
|
||||
DateFilterConditionPB.DateIsNotEmpty) ...[
|
||||
const VSpace(4),
|
||||
_buildFilterDateField(context),
|
||||
],
|
||||
];
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 1),
|
||||
child: IntrinsicHeight(child: Column(children: children)),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildFilterPanel(BuildContext context) {
|
||||
final state = context.watch<DateFilterEditorBloc>().state;
|
||||
return SizedBox(
|
||||
height: 20,
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: FlowyText(
|
||||
state.filterInfo.fieldInfo.name,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
const HSpace(4),
|
||||
Expanded(
|
||||
child: DateFilterConditionPBList(
|
||||
filterInfo: state.filterInfo,
|
||||
popoverMutex: popoverMutex,
|
||||
onCondition: (condition) {
|
||||
context
|
||||
.read<DateFilterEditorBloc>()
|
||||
.add(DateFilterEditorEvent.updateCondition(condition));
|
||||
_popover.close();
|
||||
},
|
||||
),
|
||||
),
|
||||
const HSpace(4),
|
||||
DisclosureButton(
|
||||
popoverMutex: popoverMutex,
|
||||
onAction: (action) {
|
||||
switch (action) {
|
||||
case FilterDisclosureAction.delete:
|
||||
context
|
||||
.read<DateFilterEditorBloc>()
|
||||
.add(const DateFilterEditorEvent.delete());
|
||||
break;
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildFilterDateField(BuildContext context) {
|
||||
final filter = context.watch<DateFilterEditorBloc>().state.filter;
|
||||
|
||||
final isRange = filter.condition == DateFilterConditionPB.DateWithIn;
|
||||
String? text;
|
||||
|
||||
if (isRange) {
|
||||
text =
|
||||
"${filter.start.dateTime.defaultFormat ?? ""} - ${filter.end.dateTime.defaultFormat ?? ""}";
|
||||
text = text == " - " ? null : text;
|
||||
} else {
|
||||
text = filter.timestamp.dateTime.defaultFormat;
|
||||
}
|
||||
_textEditingController.text = text ?? "";
|
||||
|
||||
return AppFlowyPopover(
|
||||
controller: _popover,
|
||||
triggerActions: PopoverTriggerFlags.none,
|
||||
direction: PopoverDirection.bottomWithLeftAligned,
|
||||
constraints: BoxConstraints.loose(const Size(260, 620)),
|
||||
margin: EdgeInsets.zero,
|
||||
child: FlowyTextField(
|
||||
controller: _textEditingController,
|
||||
readOnly: true,
|
||||
onTap: _popover.show,
|
||||
autoFocus: false,
|
||||
hintText: LocaleKeys.grid_field_dateTime.tr(),
|
||||
),
|
||||
popupBuilder: (_) {
|
||||
return BlocProvider.value(
|
||||
value: context.read<DateFilterEditorBloc>(),
|
||||
child: BlocBuilder<DateFilterEditorBloc, DateFilterEditorState>(
|
||||
builder: (context, state) {
|
||||
final filter = state.filter;
|
||||
final isRange =
|
||||
filter.condition == DateFilterConditionPB.DateWithIn;
|
||||
|
||||
return AppFlowyDatePicker(
|
||||
isRange: isRange,
|
||||
timeHintText: LocaleKeys.grid_field_selectTime.tr(),
|
||||
includeTime: false,
|
||||
dateFormat: DateFormatPB.Friendly,
|
||||
timeFormat: TimeFormatPB.TwentyFourHour,
|
||||
selectedDay: filter.timestamp.dateTime,
|
||||
startDay: isRange ? filter.start.dateTime : null,
|
||||
endDay: isRange ? filter.end.dateTime : null,
|
||||
onDaySelected: (selectedDay, _) {
|
||||
Function(DateTime) event =
|
||||
(date) => DateFilterEditorEvent.updateDate(date);
|
||||
if (isRange) {
|
||||
event = (date) =>
|
||||
DateFilterEditorEvent.updateRange(start: date);
|
||||
}
|
||||
|
||||
context.read<DateFilterEditorBloc>().add(event(selectedDay));
|
||||
if (isRange) {
|
||||
_popover.close();
|
||||
}
|
||||
},
|
||||
onRangeSelected: (start, end, _) =>
|
||||
context.read<DateFilterEditorBloc>().add(
|
||||
DateFilterEditorEvent.updateRange(
|
||||
start: start,
|
||||
end: end,
|
||||
),
|
||||
),
|
||||
onIncludeTimeChanged: (_) => {},
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class DateFilterConditionPBList extends StatelessWidget {
|
||||
const DateFilterConditionPBList({
|
||||
super.key,
|
||||
required this.filterInfo,
|
||||
required this.popoverMutex,
|
||||
required this.onCondition,
|
||||
});
|
||||
|
||||
final FilterInfo filterInfo;
|
||||
final PopoverMutex popoverMutex;
|
||||
final Function(DateFilterConditionPB) onCondition;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final dateFilter = filterInfo.dateFilter()!;
|
||||
return PopoverActionList<ConditionWrapper>(
|
||||
asBarrier: true,
|
||||
mutex: popoverMutex,
|
||||
direction: PopoverDirection.bottomWithCenterAligned,
|
||||
actions: DateFilterConditionPB.values
|
||||
.map(
|
||||
(action) => ConditionWrapper(
|
||||
action,
|
||||
dateFilter.condition == action,
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
buildChild: (controller) {
|
||||
return ConditionButton(
|
||||
conditionName: dateFilter.condition.filterName,
|
||||
onTap: () => controller.show(),
|
||||
);
|
||||
},
|
||||
onSelected: (action, controller) {
|
||||
onCondition(action.inner);
|
||||
controller.close();
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ConditionWrapper extends ActionCell {
|
||||
ConditionWrapper(this.inner, this.isSelected);
|
||||
|
||||
final DateFilterConditionPB inner;
|
||||
final bool isSelected;
|
||||
|
||||
@override
|
||||
Widget? rightIcon(Color iconColor) =>
|
||||
isSelected ? const FlowySvg(FlowySvgs.check_s) : null;
|
||||
|
||||
@override
|
||||
String get name => inner.filterName;
|
||||
}
|
||||
|
||||
extension DateFilterConditionPBExtension on DateFilterConditionPB {
|
||||
String get filterName {
|
||||
return switch (this) {
|
||||
DateFilterConditionPB.DateIs => LocaleKeys.grid_dateFilter_is.tr(),
|
||||
DateFilterConditionPB.DateBefore =>
|
||||
LocaleKeys.grid_dateFilter_before.tr(),
|
||||
DateFilterConditionPB.DateAfter => LocaleKeys.grid_dateFilter_after.tr(),
|
||||
DateFilterConditionPB.DateOnOrBefore =>
|
||||
LocaleKeys.grid_dateFilter_onOrBefore.tr(),
|
||||
DateFilterConditionPB.DateOnOrAfter =>
|
||||
LocaleKeys.grid_dateFilter_onOrAfter.tr(),
|
||||
DateFilterConditionPB.DateWithIn =>
|
||||
LocaleKeys.grid_dateFilter_between.tr(),
|
||||
DateFilterConditionPB.DateIsEmpty =>
|
||||
LocaleKeys.grid_dateFilter_empty.tr(),
|
||||
DateFilterConditionPB.DateIsNotEmpty =>
|
||||
LocaleKeys.grid_dateFilter_notEmpty.tr(),
|
||||
_ => "",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
extension DateTimeChoicechipExtension on DateTime {
|
||||
DateTime get considerLocal {
|
||||
return DateTime(year, month, day);
|
||||
}
|
||||
}
|
||||
|
||||
extension DateTimeDefaultFormatExtension on DateTime? {
|
||||
String? get defaultFormat {
|
||||
return this != null ? DateFormat('dd/MM/yyyy').format(this!) : null;
|
||||
}
|
||||
}
|
||||
|
@ -2,4 +2,7 @@ import 'package:fixnum/fixnum.dart';
|
||||
|
||||
extension DateConversion on Int64 {
|
||||
DateTime toDateTime() => DateTime.fromMillisecondsSinceEpoch(toInt() * 1000);
|
||||
|
||||
DateTime? get dateTime =>
|
||||
toInt() != 0 ? DateTime.fromMillisecondsSinceEpoch(toInt() * 1000) : null;
|
||||
}
|
||||
|
@ -39,6 +39,7 @@ class FlowyTextField extends StatefulWidget {
|
||||
final bool readOnly;
|
||||
final Color? enableBorderColor;
|
||||
final BorderRadius? borderRadius;
|
||||
final void Function()? onTap;
|
||||
final Function(PointerDownEvent)? onTapOutside;
|
||||
|
||||
const FlowyTextField({
|
||||
@ -77,6 +78,7 @@ class FlowyTextField extends StatefulWidget {
|
||||
this.readOnly = false,
|
||||
this.enableBorderColor,
|
||||
this.borderRadius,
|
||||
this.onTap,
|
||||
this.onTapOutside,
|
||||
});
|
||||
|
||||
@ -163,6 +165,7 @@ class FlowyTextFieldState extends State<FlowyTextField> {
|
||||
},
|
||||
onSubmitted: _onSubmitted,
|
||||
onEditingComplete: widget.onEditingComplete,
|
||||
onTap: widget.onTap,
|
||||
onTapOutside: widget.onTapOutside,
|
||||
minLines: 1,
|
||||
maxLines: widget.maxLines,
|
||||
|
Loading…
x
Reference in New Issue
Block a user