mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
feat: timer field (#5349)
* feat: wip timer field * feat: timer field fixing errors * feat: wip timer field frontend * fix: parsing TimerCellDataPB * feat: parse time string to minutes * fix: don't allow none number input * fix: timer filter * style: cargo fmt * fix: clippy errors * refactor: rename field type timer to time * refactor: missed some variable and files to rename * style: cargo fmt fix * feat: format time field type data in frontend * style: fix cargo fmt * fix: fixes after merge --------- Co-authored-by: Mathias Mogensen <mathiasrieckm@gmail.com>
This commit is contained in:
parent
2d4300e931
commit
aa621a8d84
@ -28,6 +28,7 @@ const mobileSupportedFieldTypes = [
|
||||
FieldType.CreatedTime,
|
||||
FieldType.Checkbox,
|
||||
FieldType.Checklist,
|
||||
FieldType.Time,
|
||||
];
|
||||
|
||||
Future<FieldType?> showFieldTypeGridBottomSheet(
|
||||
|
@ -119,6 +119,7 @@ class FieldOptionValues {
|
||||
case FieldType.RichText:
|
||||
case FieldType.URL:
|
||||
case FieldType.Checkbox:
|
||||
case FieldType.Time:
|
||||
return null;
|
||||
case FieldType.Number:
|
||||
return NumberTypeOptionPB(
|
||||
|
@ -0,0 +1,117 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart';
|
||||
import 'package:appflowy/plugins/database/application/field/field_info.dart';
|
||||
import 'package:appflowy/util/time.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
|
||||
|
||||
part 'time_cell_bloc.freezed.dart';
|
||||
|
||||
class TimeCellBloc extends Bloc<TimeCellEvent, TimeCellState> {
|
||||
TimeCellBloc({
|
||||
required this.cellController,
|
||||
}) : super(TimeCellState.initial(cellController)) {
|
||||
_dispatch();
|
||||
_startListening();
|
||||
}
|
||||
|
||||
final TimeCellController cellController;
|
||||
void Function()? _onCellChangedFn;
|
||||
|
||||
@override
|
||||
Future<void> close() async {
|
||||
if (_onCellChangedFn != null) {
|
||||
cellController.removeListener(
|
||||
onCellChanged: _onCellChangedFn!,
|
||||
onFieldChanged: _onFieldChangedListener,
|
||||
);
|
||||
}
|
||||
await cellController.dispose();
|
||||
return super.close();
|
||||
}
|
||||
|
||||
void _dispatch() {
|
||||
on<TimeCellEvent>(
|
||||
(event, emit) async {
|
||||
await event.when(
|
||||
didReceiveCellUpdate: (content) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
content:
|
||||
content != null ? formatTime(content.time.toInt()) : "",
|
||||
),
|
||||
);
|
||||
},
|
||||
didUpdateField: (fieldInfo) {
|
||||
final wrap = fieldInfo.wrapCellContent;
|
||||
if (wrap != null) {
|
||||
emit(state.copyWith(wrap: wrap));
|
||||
}
|
||||
},
|
||||
updateCell: (text) async {
|
||||
text = parseTime(text)?.toString() ?? text;
|
||||
if (state.content != text) {
|
||||
emit(state.copyWith(content: text));
|
||||
await cellController.saveCellData(text);
|
||||
|
||||
// If the input content is "abc" that can't parsered as number
|
||||
// then the data stored in the backend will be an empty string.
|
||||
// So for every cell data that will be formatted in the backend.
|
||||
// It needs to get the formatted data after saving.
|
||||
add(
|
||||
TimeCellEvent.didReceiveCellUpdate(
|
||||
cellController.getCellData(),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void _startListening() {
|
||||
_onCellChangedFn = cellController.addListener(
|
||||
onCellChanged: (cellContent) {
|
||||
if (!isClosed) {
|
||||
add(TimeCellEvent.didReceiveCellUpdate(cellContent));
|
||||
}
|
||||
},
|
||||
onFieldChanged: _onFieldChangedListener,
|
||||
);
|
||||
}
|
||||
|
||||
void _onFieldChangedListener(FieldInfo fieldInfo) {
|
||||
if (!isClosed) {
|
||||
add(TimeCellEvent.didUpdateField(fieldInfo));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@freezed
|
||||
class TimeCellEvent with _$TimeCellEvent {
|
||||
const factory TimeCellEvent.didReceiveCellUpdate(TimeCellDataPB? cell) =
|
||||
_DidReceiveCellUpdate;
|
||||
const factory TimeCellEvent.didUpdateField(FieldInfo fieldInfo) =
|
||||
_DidUpdateField;
|
||||
const factory TimeCellEvent.updateCell(String text) = _UpdateCell;
|
||||
}
|
||||
|
||||
@freezed
|
||||
class TimeCellState with _$TimeCellState {
|
||||
const factory TimeCellState({
|
||||
required String content,
|
||||
required bool wrap,
|
||||
}) = _TimeCellState;
|
||||
|
||||
factory TimeCellState.initial(TimeCellController cellController) {
|
||||
final wrap = cellController.fieldInfo.wrapCellContent;
|
||||
final cellData = cellController.getCellData();
|
||||
return TimeCellState(
|
||||
content: cellData != null ? formatTime(cellData.time.toInt()) : "",
|
||||
wrap: wrap ?? true,
|
||||
);
|
||||
}
|
||||
}
|
@ -16,6 +16,7 @@ typedef TimestampCellController = CellController<TimestampCellDataPB, String>;
|
||||
typedef URLCellController = CellController<URLCellDataPB, String>;
|
||||
typedef RelationCellController = CellController<RelationCellDataPB, String>;
|
||||
typedef SummaryCellController = CellController<String, String>;
|
||||
typedef TimeCellController = CellController<TimeCellDataPB, String>;
|
||||
typedef TranslateCellController = CellController<String, String>;
|
||||
|
||||
CellController makeCellController(
|
||||
@ -121,7 +122,6 @@ CellController makeCellController(
|
||||
),
|
||||
cellDataPersistence: TextCellDataPersistence(),
|
||||
);
|
||||
|
||||
case FieldType.Relation:
|
||||
return RelationCellController(
|
||||
viewId: viewId,
|
||||
@ -146,6 +146,18 @@ CellController makeCellController(
|
||||
),
|
||||
cellDataPersistence: TextCellDataPersistence(),
|
||||
);
|
||||
case FieldType.Time:
|
||||
return TimeCellController(
|
||||
viewId: viewId,
|
||||
fieldController: fieldController,
|
||||
cellContext: cellContext,
|
||||
rowCache: rowCache,
|
||||
cellDataLoader: CellDataLoader(
|
||||
parser: TimeCellDataParser(),
|
||||
reloadOnFieldChange: true,
|
||||
),
|
||||
cellDataPersistence: TextCellDataPersistence(),
|
||||
);
|
||||
case FieldType.Translate:
|
||||
return TranslateCellController(
|
||||
viewId: viewId,
|
||||
|
@ -181,3 +181,18 @@ class RelationCellDataParser implements CellDataParser<RelationCellDataPB> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class TimeCellDataParser implements CellDataParser<TimeCellDataPB> {
|
||||
@override
|
||||
TimeCellDataPB? parserData(List<int> data) {
|
||||
if (data.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return TimeCellDataPB.fromBuffer(data);
|
||||
} catch (e) {
|
||||
Log.error("Failed to parse timer data: $e");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -64,6 +64,7 @@ class FieldInfo with _$FieldInfo {
|
||||
case FieldType.SingleSelect:
|
||||
case FieldType.Checklist:
|
||||
case FieldType.URL:
|
||||
case FieldType.Time:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
@ -85,6 +86,7 @@ class FieldInfo with _$FieldInfo {
|
||||
case FieldType.LastEditedTime:
|
||||
case FieldType.CreatedTime:
|
||||
case FieldType.Checklist:
|
||||
case FieldType.Time:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
|
@ -202,6 +202,30 @@ class FilterBackendService {
|
||||
);
|
||||
}
|
||||
|
||||
Future<FlowyResult<void, FlowyError>> insertTimeFilter({
|
||||
required String fieldId,
|
||||
String? filterId,
|
||||
required NumberFilterConditionPB condition,
|
||||
String content = "",
|
||||
}) {
|
||||
final filter = TimeFilterPB()
|
||||
..condition = condition
|
||||
..content = content;
|
||||
|
||||
return filterId == null
|
||||
? insertFilter(
|
||||
fieldId: fieldId,
|
||||
fieldType: FieldType.Time,
|
||||
data: filter.writeToBuffer(),
|
||||
)
|
||||
: updateFilter(
|
||||
filterId: filterId,
|
||||
fieldId: fieldId,
|
||||
fieldType: FieldType.Time,
|
||||
data: filter.writeToBuffer(),
|
||||
);
|
||||
}
|
||||
|
||||
Future<FlowyResult<void, FlowyError>> insertFilter({
|
||||
required String fieldId,
|
||||
required FieldType fieldType,
|
||||
|
@ -127,6 +127,11 @@ class GridCreateFilterBloc
|
||||
fieldId: fieldId,
|
||||
condition: NumberFilterConditionPB.Equal,
|
||||
);
|
||||
case FieldType.Time:
|
||||
return _filterBackendSvc.insertTimeFilter(
|
||||
fieldId: fieldId,
|
||||
condition: NumberFilterConditionPB.Equal,
|
||||
);
|
||||
case FieldType.RichText:
|
||||
return _filterBackendSvc.insertTextFilter(
|
||||
fieldId: fieldId,
|
||||
|
@ -0,0 +1,111 @@
|
||||
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 'time_filter_editor_bloc.freezed.dart';
|
||||
|
||||
class TimeFilterEditorBloc
|
||||
extends Bloc<TimeFilterEditorEvent, TimeFilterEditorState> {
|
||||
TimeFilterEditorBloc({required this.filterInfo})
|
||||
: _filterBackendSvc = FilterBackendService(viewId: filterInfo.viewId),
|
||||
_listener = FilterListener(
|
||||
viewId: filterInfo.viewId,
|
||||
filterId: filterInfo.filter.id,
|
||||
),
|
||||
super(TimeFilterEditorState.initial(filterInfo)) {
|
||||
_dispatch();
|
||||
_startListening();
|
||||
}
|
||||
|
||||
final FilterInfo filterInfo;
|
||||
final FilterBackendService _filterBackendSvc;
|
||||
final FilterListener _listener;
|
||||
|
||||
void _dispatch() {
|
||||
on<TimeFilterEditorEvent>(
|
||||
(event, emit) async {
|
||||
event.when(
|
||||
didReceiveFilter: (filter) {
|
||||
final filterInfo = state.filterInfo.copyWith(filter: filter);
|
||||
emit(
|
||||
state.copyWith(
|
||||
filterInfo: filterInfo,
|
||||
filter: filterInfo.timeFilter()!,
|
||||
),
|
||||
);
|
||||
},
|
||||
updateCondition: (NumberFilterConditionPB condition) {
|
||||
_filterBackendSvc.insertTimeFilter(
|
||||
filterId: filterInfo.filter.id,
|
||||
fieldId: filterInfo.fieldInfo.id,
|
||||
condition: condition,
|
||||
content: state.filter.content,
|
||||
);
|
||||
},
|
||||
updateContent: (content) {
|
||||
_filterBackendSvc.insertTimeFilter(
|
||||
filterId: filterInfo.filter.id,
|
||||
fieldId: filterInfo.fieldInfo.id,
|
||||
condition: state.filter.condition,
|
||||
content: content,
|
||||
);
|
||||
},
|
||||
delete: () {
|
||||
_filterBackendSvc.deleteFilter(
|
||||
fieldId: filterInfo.fieldInfo.id,
|
||||
filterId: filterInfo.filter.id,
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void _startListening() {
|
||||
_listener.start(
|
||||
onUpdated: (filter) {
|
||||
if (!isClosed) {
|
||||
add(TimeFilterEditorEvent.didReceiveFilter(filter));
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> close() async {
|
||||
await _listener.stop();
|
||||
return super.close();
|
||||
}
|
||||
}
|
||||
|
||||
@freezed
|
||||
class TimeFilterEditorEvent with _$TimeFilterEditorEvent {
|
||||
const factory TimeFilterEditorEvent.didReceiveFilter(FilterPB filter) =
|
||||
_DidReceiveFilter;
|
||||
const factory TimeFilterEditorEvent.updateCondition(
|
||||
NumberFilterConditionPB condition,
|
||||
) = _UpdateCondition;
|
||||
const factory TimeFilterEditorEvent.updateContent(String content) =
|
||||
_UpdateContent;
|
||||
const factory TimeFilterEditorEvent.delete() = _Delete;
|
||||
}
|
||||
|
||||
@freezed
|
||||
class TimeFilterEditorState with _$TimeFilterEditorState {
|
||||
const factory TimeFilterEditorState({
|
||||
required FilterInfo filterInfo,
|
||||
required TimeFilterPB filter,
|
||||
}) = _TimeFilterEditorState;
|
||||
|
||||
factory TimeFilterEditorState.initial(FilterInfo filterInfo) {
|
||||
return TimeFilterEditorState(
|
||||
filterInfo: filterInfo,
|
||||
filter: filterInfo.timeFilter()!,
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,227 @@
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/database/grid/application/filter/time_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: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 '../condition_button.dart';
|
||||
import '../disclosure_button.dart';
|
||||
import '../filter_info.dart';
|
||||
import 'choicechip.dart';
|
||||
|
||||
class TimeFilterChoiceChip extends StatefulWidget {
|
||||
const TimeFilterChoiceChip({
|
||||
super.key,
|
||||
required this.filterInfo,
|
||||
});
|
||||
|
||||
final FilterInfo filterInfo;
|
||||
|
||||
@override
|
||||
State<TimeFilterChoiceChip> createState() => _TimeFilterChoiceChipState();
|
||||
}
|
||||
|
||||
class _TimeFilterChoiceChipState extends State<TimeFilterChoiceChip> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider(
|
||||
create: (_) => TimeFilterEditorBloc(
|
||||
filterInfo: widget.filterInfo,
|
||||
),
|
||||
child: BlocBuilder<TimeFilterEditorBloc, TimeFilterEditorState>(
|
||||
builder: (context, state) {
|
||||
return AppFlowyPopover(
|
||||
constraints: BoxConstraints.loose(const Size(200, 100)),
|
||||
direction: PopoverDirection.bottomWithCenterAligned,
|
||||
popupBuilder: (_) {
|
||||
return BlocProvider.value(
|
||||
value: context.read<TimeFilterEditorBloc>(),
|
||||
child: const TimeFilterEditor(),
|
||||
);
|
||||
},
|
||||
child: ChoiceChipButton(
|
||||
filterInfo: state.filterInfo,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class TimeFilterEditor extends StatefulWidget {
|
||||
const TimeFilterEditor({super.key});
|
||||
|
||||
@override
|
||||
State<TimeFilterEditor> createState() => _TimeFilterEditorState();
|
||||
}
|
||||
|
||||
class _TimeFilterEditorState extends State<TimeFilterEditor> {
|
||||
final popoverMutex = PopoverMutex();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<TimeFilterEditorBloc, TimeFilterEditorState>(
|
||||
builder: (context, state) {
|
||||
final List<Widget> children = [
|
||||
_buildFilterPanel(context, state),
|
||||
if (state.filter.condition != NumberFilterConditionPB.NumberIsEmpty &&
|
||||
state.filter.condition !=
|
||||
NumberFilterConditionPB.NumberIsNotEmpty) ...[
|
||||
const VSpace(4),
|
||||
_buildFilterTimeField(context, state),
|
||||
],
|
||||
];
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 1),
|
||||
child: IntrinsicHeight(child: Column(children: children)),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildFilterPanel(
|
||||
BuildContext context,
|
||||
TimeFilterEditorState state,
|
||||
) {
|
||||
return SizedBox(
|
||||
height: 20,
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: FlowyText(
|
||||
state.filterInfo.fieldInfo.name,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
const HSpace(4),
|
||||
Expanded(
|
||||
child: TimeFilterConditionPBList(
|
||||
filterInfo: state.filterInfo,
|
||||
popoverMutex: popoverMutex,
|
||||
onCondition: (condition) {
|
||||
context
|
||||
.read<TimeFilterEditorBloc>()
|
||||
.add(TimeFilterEditorEvent.updateCondition(condition));
|
||||
},
|
||||
),
|
||||
),
|
||||
const HSpace(4),
|
||||
DisclosureButton(
|
||||
popoverMutex: popoverMutex,
|
||||
onAction: (action) {
|
||||
switch (action) {
|
||||
case FilterDisclosureAction.delete:
|
||||
context
|
||||
.read<TimeFilterEditorBloc>()
|
||||
.add(const TimeFilterEditorEvent.delete());
|
||||
break;
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildFilterTimeField(
|
||||
BuildContext context,
|
||||
TimeFilterEditorState state,
|
||||
) {
|
||||
return FlowyTextField(
|
||||
text: state.filter.content,
|
||||
hintText: LocaleKeys.grid_settings_typeAValue.tr(),
|
||||
debounceDuration: const Duration(milliseconds: 300),
|
||||
autoFocus: false,
|
||||
onChanged: (text) {
|
||||
context
|
||||
.read<TimeFilterEditorBloc>()
|
||||
.add(TimeFilterEditorEvent.updateContent(text));
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class TimeFilterConditionPBList extends StatelessWidget {
|
||||
const TimeFilterConditionPBList({
|
||||
super.key,
|
||||
required this.filterInfo,
|
||||
required this.popoverMutex,
|
||||
required this.onCondition,
|
||||
});
|
||||
|
||||
final FilterInfo filterInfo;
|
||||
final PopoverMutex popoverMutex;
|
||||
final Function(NumberFilterConditionPB) onCondition;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final timeFilter = filterInfo.timeFilter()!;
|
||||
return PopoverActionList<ConditionWrapper>(
|
||||
asBarrier: true,
|
||||
mutex: popoverMutex,
|
||||
direction: PopoverDirection.bottomWithCenterAligned,
|
||||
actions: NumberFilterConditionPB.values
|
||||
.map(
|
||||
(action) => ConditionWrapper(
|
||||
action,
|
||||
timeFilter.condition == action,
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
buildChild: (controller) {
|
||||
return ConditionButton(
|
||||
conditionName: timeFilter.condition.filterName,
|
||||
onTap: () => controller.show(),
|
||||
);
|
||||
},
|
||||
onSelected: (action, controller) {
|
||||
onCondition(action.inner);
|
||||
controller.close();
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ConditionWrapper extends ActionCell {
|
||||
ConditionWrapper(this.inner, this.isSelected);
|
||||
|
||||
final NumberFilterConditionPB inner;
|
||||
final bool isSelected;
|
||||
|
||||
@override
|
||||
Widget? rightIcon(Color iconColor) =>
|
||||
isSelected ? const FlowySvg(FlowySvgs.check_s) : null;
|
||||
|
||||
@override
|
||||
String get name => inner.filterName;
|
||||
}
|
||||
|
||||
extension TimeFilterConditionPBExtension on NumberFilterConditionPB {
|
||||
String get filterName {
|
||||
return switch (this) {
|
||||
NumberFilterConditionPB.Equal => LocaleKeys.grid_numberFilter_equal.tr(),
|
||||
NumberFilterConditionPB.NotEqual =>
|
||||
LocaleKeys.grid_numberFilter_notEqual.tr(),
|
||||
NumberFilterConditionPB.LessThan =>
|
||||
LocaleKeys.grid_numberFilter_lessThan.tr(),
|
||||
NumberFilterConditionPB.LessThanOrEqualTo =>
|
||||
LocaleKeys.grid_numberFilter_lessThanOrEqualTo.tr(),
|
||||
NumberFilterConditionPB.GreaterThan =>
|
||||
LocaleKeys.grid_numberFilter_greaterThan.tr(),
|
||||
NumberFilterConditionPB.GreaterThanOrEqualTo =>
|
||||
LocaleKeys.grid_numberFilter_greaterThanOrEqualTo.tr(),
|
||||
NumberFilterConditionPB.NumberIsEmpty =>
|
||||
LocaleKeys.grid_numberFilter_isEmpty.tr(),
|
||||
NumberFilterConditionPB.NumberIsNotEmpty =>
|
||||
LocaleKeys.grid_numberFilter_isNotEmpty.tr(),
|
||||
_ => "",
|
||||
};
|
||||
}
|
||||
}
|
@ -60,4 +60,10 @@ class FilterInfo {
|
||||
? NumberFilterPB.fromBuffer(filter.data.data)
|
||||
: null;
|
||||
}
|
||||
|
||||
TimeFilterPB? timeFilter() {
|
||||
return filter.data.fieldType == FieldType.Time
|
||||
? TimeFilterPB.fromBuffer(filter.data.data)
|
||||
: null;
|
||||
}
|
||||
}
|
||||
|
@ -8,6 +8,7 @@ import 'choicechip/number.dart';
|
||||
import 'choicechip/select_option/select_option.dart';
|
||||
import 'choicechip/text.dart';
|
||||
import 'choicechip/url.dart';
|
||||
import 'choicechip/time.dart';
|
||||
import 'filter_info.dart';
|
||||
|
||||
class FilterMenuItem extends StatelessWidget {
|
||||
@ -22,12 +23,15 @@ class FilterMenuItem extends StatelessWidget {
|
||||
FieldType.DateTime => DateFilterChoicechip(filterInfo: filterInfo),
|
||||
FieldType.MultiSelect =>
|
||||
SelectOptionFilterChoicechip(filterInfo: filterInfo),
|
||||
FieldType.Number => NumberFilterChoiceChip(filterInfo: filterInfo),
|
||||
FieldType.Number =>
|
||||
NumberFilterChoiceChip(filterInfo: filterInfo),
|
||||
FieldType.RichText => TextFilterChoicechip(filterInfo: filterInfo),
|
||||
FieldType.SingleSelect =>
|
||||
SelectOptionFilterChoicechip(filterInfo: filterInfo),
|
||||
FieldType.URL => URLFilterChoiceChip(filterInfo: filterInfo),
|
||||
FieldType.Checklist => ChecklistFilterChoicechip(filterInfo: filterInfo),
|
||||
FieldType.Time =>
|
||||
TimeFilterChoiceChip(filterInfo: filterInfo),
|
||||
_ => const SizedBox(),
|
||||
};
|
||||
}
|
||||
|
@ -1,19 +1,21 @@
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
import 'package:appflowy/plugins/database/application/cell/cell_controller.dart';
|
||||
import 'package:appflowy/plugins/database/application/database_controller.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/cell/card_cell_skeleton/relation_card_cell.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/cell/card_cell_skeleton/timestamp_card_cell.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/cell/card_cell_skeleton/translate_card_cell.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
import 'card_cell_skeleton/card_cell.dart';
|
||||
import 'card_cell_skeleton/checkbox_card_cell.dart';
|
||||
import 'card_cell_skeleton/checklist_card_cell.dart';
|
||||
import 'card_cell_skeleton/date_card_cell.dart';
|
||||
import 'card_cell_skeleton/number_card_cell.dart';
|
||||
import 'card_cell_skeleton/relation_card_cell.dart';
|
||||
import 'card_cell_skeleton/select_option_card_cell.dart';
|
||||
import 'card_cell_skeleton/summary_card_cell.dart';
|
||||
import 'card_cell_skeleton/text_card_cell.dart';
|
||||
import 'card_cell_skeleton/time_card_cell.dart';
|
||||
import 'card_cell_skeleton/timestamp_card_cell.dart';
|
||||
import 'card_cell_skeleton/translate_card_cell.dart';
|
||||
import 'card_cell_skeleton/url_card_cell.dart';
|
||||
|
||||
typedef CardCellStyleMap = Map<FieldType, CardCellStyle>;
|
||||
@ -99,6 +101,12 @@ class CardCellBuilder {
|
||||
databaseController: databaseController,
|
||||
cellContext: cellContext,
|
||||
),
|
||||
FieldType.Time => TimeCardCell(
|
||||
key: key,
|
||||
style: isStyleOrNull(style),
|
||||
databaseController: databaseController,
|
||||
cellContext: cellContext,
|
||||
),
|
||||
FieldType.Translate => TranslateCardCell(
|
||||
key: key,
|
||||
style: isStyleOrNull(style),
|
||||
|
@ -0,0 +1,62 @@
|
||||
import 'package:appflowy/plugins/database/application/cell/cell_controller.dart';
|
||||
import 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart';
|
||||
import 'package:appflowy/plugins/database/application/database_controller.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:appflowy/plugins/database/application/cell/bloc/time_cell_bloc.dart';
|
||||
|
||||
import 'card_cell.dart';
|
||||
|
||||
class TimeCardCellStyle extends CardCellStyle {
|
||||
const TimeCardCellStyle({
|
||||
required super.padding,
|
||||
required this.textStyle,
|
||||
});
|
||||
|
||||
final TextStyle textStyle;
|
||||
}
|
||||
|
||||
class TimeCardCell extends CardCell<TimeCardCellStyle> {
|
||||
const TimeCardCell({
|
||||
super.key,
|
||||
required super.style,
|
||||
required this.databaseController,
|
||||
required this.cellContext,
|
||||
});
|
||||
|
||||
final DatabaseController databaseController;
|
||||
final CellContext cellContext;
|
||||
|
||||
@override
|
||||
State<TimeCardCell> createState() => _TimeCellState();
|
||||
}
|
||||
|
||||
class _TimeCellState extends State<TimeCardCell> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider(
|
||||
create: (context) {
|
||||
return TimeCellBloc(
|
||||
cellController: makeCellController(
|
||||
widget.databaseController,
|
||||
widget.cellContext,
|
||||
).as(),
|
||||
);
|
||||
},
|
||||
child: BlocBuilder<TimeCellBloc, TimeCellState>(
|
||||
buildWhen: (previous, current) => previous.content != current.content,
|
||||
builder: (context, state) {
|
||||
if (state.content.isEmpty) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return Container(
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
padding: widget.style.padding,
|
||||
child: Text(state.content, style: widget.style.textStyle),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:appflowy/plugins/database/widgets/cell/card_cell_skeleton/summary_card_cell.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../card_cell_builder.dart';
|
||||
import '../card_cell_skeleton/checkbox_card_cell.dart';
|
||||
@ -10,6 +11,7 @@ import '../card_cell_skeleton/number_card_cell.dart';
|
||||
import '../card_cell_skeleton/relation_card_cell.dart';
|
||||
import '../card_cell_skeleton/select_option_card_cell.dart';
|
||||
import '../card_cell_skeleton/text_card_cell.dart';
|
||||
import '../card_cell_skeleton/time_card_cell.dart';
|
||||
import '../card_cell_skeleton/timestamp_card_cell.dart';
|
||||
import '../card_cell_skeleton/url_card_cell.dart';
|
||||
|
||||
@ -84,6 +86,10 @@ CardCellStyleMap desktopBoardCardCellStyleMap(BuildContext context) {
|
||||
padding: padding,
|
||||
textStyle: textStyle,
|
||||
),
|
||||
FieldType.Time: TimeCardCellStyle(
|
||||
padding: padding,
|
||||
textStyle: textStyle,
|
||||
),
|
||||
FieldType.Translate: SummaryCardCellStyle(
|
||||
padding: padding,
|
||||
textStyle: textStyle,
|
||||
|
@ -1,6 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:appflowy/plugins/database/widgets/cell/card_cell_skeleton/summary_card_cell.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../card_cell_builder.dart';
|
||||
import '../card_cell_skeleton/checkbox_card_cell.dart';
|
||||
@ -10,6 +11,7 @@ import '../card_cell_skeleton/number_card_cell.dart';
|
||||
import '../card_cell_skeleton/relation_card_cell.dart';
|
||||
import '../card_cell_skeleton/select_option_card_cell.dart';
|
||||
import '../card_cell_skeleton/text_card_cell.dart';
|
||||
import '../card_cell_skeleton/time_card_cell.dart';
|
||||
import '../card_cell_skeleton/timestamp_card_cell.dart';
|
||||
import '../card_cell_skeleton/url_card_cell.dart';
|
||||
|
||||
@ -83,6 +85,10 @@ CardCellStyleMap mobileBoardCardCellStyleMap(BuildContext context) {
|
||||
padding: padding,
|
||||
textStyle: textStyle,
|
||||
),
|
||||
FieldType.Time: TimeCardCellStyle(
|
||||
padding: padding,
|
||||
textStyle: textStyle,
|
||||
),
|
||||
FieldType.Translate: SummaryCardCellStyle(
|
||||
padding: padding,
|
||||
textStyle: textStyle,
|
||||
|
@ -0,0 +1,37 @@
|
||||
import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart';
|
||||
import 'package:appflowy/plugins/database/application/cell/bloc/time_cell_bloc.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import '../editable_cell_skeleton/time.dart';
|
||||
|
||||
class DesktopGridTimeCellSkin extends IEditableTimeCellSkin {
|
||||
@override
|
||||
Widget build(
|
||||
BuildContext context,
|
||||
CellContainerNotifier cellContainerNotifier,
|
||||
TimeCellBloc bloc,
|
||||
FocusNode focusNode,
|
||||
TextEditingController textEditingController,
|
||||
) {
|
||||
return TextField(
|
||||
controller: textEditingController,
|
||||
focusNode: focusNode,
|
||||
onEditingComplete: () => focusNode.unfocus(),
|
||||
onSubmitted: (_) => focusNode.unfocus(),
|
||||
maxLines: context.watch<TimeCellBloc>().state.wrap ? null : 1,
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
textInputAction: TextInputAction.done,
|
||||
decoration: InputDecoration(
|
||||
contentPadding: GridSize.cellContentInsets,
|
||||
border: InputBorder.none,
|
||||
focusedBorder: InputBorder.none,
|
||||
enabledBorder: InputBorder.none,
|
||||
errorBorder: InputBorder.none,
|
||||
disabledBorder: InputBorder.none,
|
||||
isDense: true,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,40 @@
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart';
|
||||
import 'package:appflowy/plugins/database/application/cell/bloc/time_cell_bloc.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../editable_cell_skeleton/time.dart';
|
||||
|
||||
class DesktopRowDetailTimeCellSkin extends IEditableTimeCellSkin {
|
||||
@override
|
||||
Widget build(
|
||||
BuildContext context,
|
||||
CellContainerNotifier cellContainerNotifier,
|
||||
TimeCellBloc bloc,
|
||||
FocusNode focusNode,
|
||||
TextEditingController textEditingController,
|
||||
) {
|
||||
return TextField(
|
||||
controller: textEditingController,
|
||||
focusNode: focusNode,
|
||||
onEditingComplete: () => focusNode.unfocus(),
|
||||
onSubmitted: (_) => focusNode.unfocus(),
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
textInputAction: TextInputAction.done,
|
||||
decoration: InputDecoration(
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 8, vertical: 9),
|
||||
border: InputBorder.none,
|
||||
focusedBorder: InputBorder.none,
|
||||
enabledBorder: InputBorder.none,
|
||||
errorBorder: InputBorder.none,
|
||||
disabledBorder: InputBorder.none,
|
||||
hintText: LocaleKeys.grid_row_textPlaceholder.tr(),
|
||||
hintStyle: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: Theme.of(context).hintColor,
|
||||
),
|
||||
isDense: true,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -1,9 +1,9 @@
|
||||
import 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/translate.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
import 'package:appflowy/plugins/database/application/cell/cell_controller.dart';
|
||||
import 'package:appflowy/plugins/database/application/database_controller.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/translate.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
|
||||
|
||||
import '../row/accessory/cell_accessory.dart';
|
||||
@ -18,6 +18,7 @@ import 'editable_cell_skeleton/relation.dart';
|
||||
import 'editable_cell_skeleton/select_option.dart';
|
||||
import 'editable_cell_skeleton/summary.dart';
|
||||
import 'editable_cell_skeleton/text.dart';
|
||||
import 'editable_cell_skeleton/time.dart';
|
||||
import 'editable_cell_skeleton/timestamp.dart';
|
||||
import 'editable_cell_skeleton/url.dart';
|
||||
|
||||
@ -121,6 +122,12 @@ class EditableCellBuilder {
|
||||
skin: IEditableSummaryCellSkin.fromStyle(style),
|
||||
key: key,
|
||||
),
|
||||
FieldType.Time => EditableTimeCell(
|
||||
databaseController: databaseController,
|
||||
cellContext: cellContext,
|
||||
skin: IEditableTimeCellSkin.fromStyle(style),
|
||||
key: key,
|
||||
),
|
||||
FieldType.Translate => EditableTranslateCell(
|
||||
databaseController: databaseController,
|
||||
cellContext: cellContext,
|
||||
@ -213,6 +220,12 @@ class EditableCellBuilder {
|
||||
skin: skinMap.relationSkin!,
|
||||
key: key,
|
||||
),
|
||||
FieldType.Time => EditableTimeCell(
|
||||
databaseController: databaseController,
|
||||
cellContext: cellContext,
|
||||
skin: skinMap.timeSkin!,
|
||||
key: key,
|
||||
),
|
||||
_ => throw UnimplementedError(),
|
||||
};
|
||||
}
|
||||
@ -368,6 +381,7 @@ class EditableCellSkinMap {
|
||||
this.textSkin,
|
||||
this.urlSkin,
|
||||
this.relationSkin,
|
||||
this.timeSkin,
|
||||
});
|
||||
|
||||
final IEditableCheckboxCellSkin? checkboxSkin;
|
||||
@ -379,6 +393,7 @@ class EditableCellSkinMap {
|
||||
final IEditableTextCellSkin? textSkin;
|
||||
final IEditableURLCellSkin? urlSkin;
|
||||
final IEditableRelationCellSkin? relationSkin;
|
||||
final IEditableTimeCellSkin? timeSkin;
|
||||
|
||||
bool has(FieldType fieldType) {
|
||||
return switch (fieldType) {
|
||||
@ -394,6 +409,7 @@ class EditableCellSkinMap {
|
||||
FieldType.Number => numberSkin != null,
|
||||
FieldType.RichText => textSkin != null,
|
||||
FieldType.URL => urlSkin != null,
|
||||
FieldType.Time => timeSkin != null,
|
||||
_ => throw UnimplementedError(),
|
||||
};
|
||||
}
|
||||
|
@ -0,0 +1,120 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:appflowy/plugins/database/application/cell/cell_controller.dart';
|
||||
import 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart';
|
||||
import 'package:appflowy/plugins/database/application/database_controller.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart';
|
||||
import 'package:appflowy/plugins/database/application/cell/bloc/time_cell_bloc.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/cell/editable_cell_builder.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import '../desktop_grid/desktop_grid_time_cell.dart';
|
||||
import '../desktop_row_detail/desktop_row_detail_time_cell.dart';
|
||||
import '../mobile_grid/mobile_grid_time_cell.dart';
|
||||
import '../mobile_row_detail/mobile_row_detail_time_cell.dart';
|
||||
|
||||
abstract class IEditableTimeCellSkin {
|
||||
const IEditableTimeCellSkin();
|
||||
|
||||
factory IEditableTimeCellSkin.fromStyle(EditableCellStyle style) {
|
||||
return switch (style) {
|
||||
EditableCellStyle.desktopGrid => DesktopGridTimeCellSkin(),
|
||||
EditableCellStyle.desktopRowDetail => DesktopRowDetailTimeCellSkin(),
|
||||
EditableCellStyle.mobileGrid => MobileGridTimeCellSkin(),
|
||||
EditableCellStyle.mobileRowDetail => MobileRowDetailTimeCellSkin(),
|
||||
};
|
||||
}
|
||||
|
||||
Widget build(
|
||||
BuildContext context,
|
||||
CellContainerNotifier cellContainerNotifier,
|
||||
TimeCellBloc bloc,
|
||||
FocusNode focusNode,
|
||||
TextEditingController textEditingController,
|
||||
);
|
||||
}
|
||||
|
||||
class EditableTimeCell extends EditableCellWidget {
|
||||
EditableTimeCell({
|
||||
super.key,
|
||||
required this.databaseController,
|
||||
required this.cellContext,
|
||||
required this.skin,
|
||||
});
|
||||
|
||||
final DatabaseController databaseController;
|
||||
final CellContext cellContext;
|
||||
final IEditableTimeCellSkin skin;
|
||||
|
||||
@override
|
||||
GridEditableTextCell<EditableTimeCell> createState() => _TimeCellState();
|
||||
}
|
||||
|
||||
class _TimeCellState extends GridEditableTextCell<EditableTimeCell> {
|
||||
late final TextEditingController _textEditingController;
|
||||
late final cellBloc = TimeCellBloc(
|
||||
cellController: makeCellController(
|
||||
widget.databaseController,
|
||||
widget.cellContext,
|
||||
).as(),
|
||||
);
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_textEditingController =
|
||||
TextEditingController(text: cellBloc.state.content);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_textEditingController.dispose();
|
||||
cellBloc.close();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider.value(
|
||||
value: cellBloc,
|
||||
child: BlocListener<TimeCellBloc, TimeCellState>(
|
||||
listener: (context, state) =>
|
||||
_textEditingController.text = state.content,
|
||||
child: Builder(
|
||||
builder: (context) {
|
||||
return widget.skin.build(
|
||||
context,
|
||||
widget.cellContainerNotifier,
|
||||
cellBloc,
|
||||
focusNode,
|
||||
_textEditingController,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
SingleListenerFocusNode focusNode = SingleListenerFocusNode();
|
||||
|
||||
@override
|
||||
void onRequestFocus() {
|
||||
focusNode.requestFocus();
|
||||
}
|
||||
|
||||
@override
|
||||
String? onCopy() => cellBloc.state.content;
|
||||
|
||||
@override
|
||||
Future<void> focusChanged() async {
|
||||
if (mounted &&
|
||||
!cellBloc.isClosed &&
|
||||
cellBloc.state.content != _textEditingController.text.trim()) {
|
||||
cellBloc
|
||||
.add(TimeCellEvent.updateCell(_textEditingController.text.trim()));
|
||||
}
|
||||
return super.focusChanged();
|
||||
}
|
||||
}
|
@ -0,0 +1,29 @@
|
||||
import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart';
|
||||
import 'package:appflowy/plugins/database/application/cell/bloc/time_cell_bloc.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../editable_cell_skeleton/time.dart';
|
||||
|
||||
class MobileGridTimeCellSkin extends IEditableTimeCellSkin {
|
||||
@override
|
||||
Widget build(
|
||||
BuildContext context,
|
||||
CellContainerNotifier cellContainerNotifier,
|
||||
TimeCellBloc bloc,
|
||||
FocusNode focusNode,
|
||||
TextEditingController textEditingController,
|
||||
) {
|
||||
return TextField(
|
||||
controller: textEditingController,
|
||||
focusNode: focusNode,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(fontSize: 15),
|
||||
decoration: const InputDecoration(
|
||||
enabledBorder: InputBorder.none,
|
||||
focusedBorder: InputBorder.none,
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 14, vertical: 12),
|
||||
isCollapsed: true,
|
||||
),
|
||||
onTapOutside: (event) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,46 @@
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart';
|
||||
import 'package:appflowy/plugins/database/application/cell/bloc/time_cell_bloc.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../editable_cell_skeleton/time.dart';
|
||||
|
||||
class MobileRowDetailTimeCellSkin extends IEditableTimeCellSkin {
|
||||
@override
|
||||
Widget build(
|
||||
BuildContext context,
|
||||
CellContainerNotifier cellContainerNotifier,
|
||||
TimeCellBloc bloc,
|
||||
FocusNode focusNode,
|
||||
TextEditingController textEditingController,
|
||||
) {
|
||||
return TextField(
|
||||
controller: textEditingController,
|
||||
focusNode: focusNode,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(fontSize: 16),
|
||||
decoration: InputDecoration(
|
||||
enabledBorder:
|
||||
_getInputBorder(color: Theme.of(context).colorScheme.outline),
|
||||
focusedBorder:
|
||||
_getInputBorder(color: Theme.of(context).colorScheme.primary),
|
||||
hintText: LocaleKeys.grid_row_textPlaceholder.tr(),
|
||||
contentPadding:
|
||||
const EdgeInsets.symmetric(horizontal: 12, vertical: 13),
|
||||
isCollapsed: true,
|
||||
isDense: true,
|
||||
constraints: const BoxConstraints(),
|
||||
),
|
||||
// close keyboard when tapping outside of the text field
|
||||
onTapOutside: (event) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||
);
|
||||
}
|
||||
|
||||
InputBorder _getInputBorder({Color? color}) {
|
||||
return OutlineInputBorder(
|
||||
borderSide: BorderSide(color: color!),
|
||||
borderRadius: const BorderRadius.all(Radius.circular(14)),
|
||||
gapPadding: 0,
|
||||
);
|
||||
}
|
||||
}
|
@ -1,10 +1,11 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart';
|
||||
import 'package:appflowy/util/field_type_extension.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart';
|
||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
typedef SelectFieldCallback = void Function(FieldType);
|
||||
|
||||
@ -21,6 +22,7 @@ const List<FieldType> _supportedFieldTypes = [
|
||||
FieldType.CreatedTime,
|
||||
FieldType.Relation,
|
||||
FieldType.Summary,
|
||||
FieldType.Time,
|
||||
FieldType.Translate,
|
||||
];
|
||||
|
||||
|
@ -1,9 +1,10 @@
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:appflowy/plugins/database/widgets/field/type_option_editor/translate.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
|
||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'checkbox.dart';
|
||||
import 'checklist.dart';
|
||||
@ -14,6 +15,7 @@ import 'relation.dart';
|
||||
import 'rich_text.dart';
|
||||
import 'single_select.dart';
|
||||
import 'summary.dart';
|
||||
import 'time.dart';
|
||||
import 'timestamp.dart';
|
||||
import 'url.dart';
|
||||
|
||||
@ -34,6 +36,7 @@ abstract class TypeOptionEditorFactory {
|
||||
FieldType.Checklist => const ChecklistTypeOptionEditorFactory(),
|
||||
FieldType.Relation => const RelationTypeOptionEditorFactory(),
|
||||
FieldType.Summary => const SummaryTypeOptionEditorFactory(),
|
||||
FieldType.Time => const TimeTypeOptionEditorFactory(),
|
||||
FieldType.Translate => const TranslateTypeOptionEditorFactory(),
|
||||
_ => throw UnimplementedError(),
|
||||
};
|
||||
|
@ -0,0 +1,19 @@
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
|
||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'builder.dart';
|
||||
|
||||
class TimeTypeOptionEditorFactory implements TypeOptionEditorFactory {
|
||||
const TimeTypeOptionEditorFactory();
|
||||
|
||||
@override
|
||||
Widget? build({
|
||||
required BuildContext context,
|
||||
required String viewId,
|
||||
required FieldPB field,
|
||||
required PopoverMutex popoverMutex,
|
||||
required TypeOptionDataCallback onTypeOptionUpdated,
|
||||
}) =>
|
||||
null;
|
||||
}
|
@ -22,6 +22,7 @@ extension FieldTypeExtension on FieldType {
|
||||
FieldType.CreatedTime => LocaleKeys.grid_field_createdAtFieldName.tr(),
|
||||
FieldType.Relation => LocaleKeys.grid_field_relationFieldName.tr(),
|
||||
FieldType.Summary => LocaleKeys.grid_field_summaryFieldName.tr(),
|
||||
FieldType.Time => LocaleKeys.grid_field_timeFieldName.tr(),
|
||||
FieldType.Translate => LocaleKeys.grid_field_translateFieldName.tr(),
|
||||
_ => throw UnimplementedError(),
|
||||
};
|
||||
@ -39,6 +40,7 @@ extension FieldTypeExtension on FieldType {
|
||||
FieldType.CreatedTime => FlowySvgs.created_at_s,
|
||||
FieldType.Relation => FlowySvgs.relation_s,
|
||||
FieldType.Summary => FlowySvgs.ai_summary_s,
|
||||
FieldType.Time => FlowySvgs.timer_start_s,
|
||||
FieldType.Translate => FlowySvgs.ai_translate_s,
|
||||
_ => throw UnimplementedError(),
|
||||
};
|
||||
@ -62,6 +64,7 @@ extension FieldTypeExtension on FieldType {
|
||||
FieldType.Checklist => const Color(0xFF98F4CD),
|
||||
FieldType.Relation => const Color(0xFFFDEDA7),
|
||||
FieldType.Summary => const Color(0xFFBECCFF),
|
||||
FieldType.Time => const Color(0xFFFDEDA7),
|
||||
FieldType.Translate => const Color(0xFFBECCFF),
|
||||
_ => throw UnimplementedError(),
|
||||
};
|
||||
@ -80,6 +83,7 @@ extension FieldTypeExtension on FieldType {
|
||||
FieldType.Checklist => const Color(0xFF42AD93),
|
||||
FieldType.Relation => const Color(0xFFFDEDA7),
|
||||
FieldType.Summary => const Color(0xFF6859A7),
|
||||
FieldType.Time => const Color(0xFFFDEDA7),
|
||||
FieldType.Translate => const Color(0xFF6859A7),
|
||||
_ => throw UnimplementedError(),
|
||||
};
|
||||
|
43
frontend/appflowy_flutter/lib/util/time.dart
Normal file
43
frontend/appflowy_flutter/lib/util/time.dart
Normal file
@ -0,0 +1,43 @@
|
||||
final RegExp timerRegExp =
|
||||
RegExp(r'(?:(?<hours>\d*)h)? ?(?:(?<minutes>\d*)m)?');
|
||||
|
||||
int? parseTime(String timerStr) {
|
||||
int? res = int.tryParse(timerStr);
|
||||
if (res != null) {
|
||||
return res;
|
||||
}
|
||||
|
||||
final matches = timerRegExp.firstMatch(timerStr);
|
||||
if (matches == null) {
|
||||
return null;
|
||||
}
|
||||
final hours = int.tryParse(matches.namedGroup('hours') ?? "");
|
||||
final minutes = int.tryParse(matches.namedGroup('minutes') ?? "");
|
||||
if (hours == null && minutes == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final expected =
|
||||
"${hours != null ? '${hours}h' : ''}${hours != null && minutes != null ? ' ' : ''}${minutes != null ? '${minutes}m' : ''}";
|
||||
if (timerStr != expected) {
|
||||
return null;
|
||||
}
|
||||
|
||||
res = 0;
|
||||
res += hours != null ? hours * 60 : res;
|
||||
res += minutes ?? 0;
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
String formatTime(int minutes) {
|
||||
if (minutes >= 60) {
|
||||
if (minutes % 60 == 0) {
|
||||
return "${minutes ~/ 60}h";
|
||||
}
|
||||
return "${minutes ~/ 60}h ${minutes % 60}m";
|
||||
} else if (minutes >= 0) {
|
||||
return "${minutes}m";
|
||||
}
|
||||
return "";
|
||||
}
|
@ -1041,6 +1041,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.19.0"
|
||||
intl_utils:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: intl_utils
|
||||
sha256: c2b1f5c72c25512cbeef5ab015c008fc50fe7e04813ba5541c25272300484bf4
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.8.7"
|
||||
io:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -2192,6 +2200,22 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.3.2"
|
||||
universal_html:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: universal_html
|
||||
sha256: "56536254004e24d9d8cfdb7dbbf09b74cf8df96729f38a2f5c238163e3d58971"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.4"
|
||||
universal_io:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: universal_io
|
||||
sha256: "1722b2dcc462b4b2f3ee7d188dad008b6eb4c40bbd03a3de451d82c78bba9aad"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.2"
|
||||
universal_platform:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
24
frontend/appflowy_flutter/test/unit_test/util/time.dart
Normal file
24
frontend/appflowy_flutter/test/unit_test/util/time.dart
Normal file
@ -0,0 +1,24 @@
|
||||
import 'package:appflowy/util/time.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
void main() {
|
||||
test('parseTime should parse time string to minutes', () {
|
||||
expect(parseTime('10'), 10);
|
||||
expect(parseTime('70m'), 70);
|
||||
expect(parseTime('4h 20m'), 260);
|
||||
expect(parseTime('1h 80m'), 140);
|
||||
expect(parseTime('asffsa2h3m'), null);
|
||||
expect(parseTime('2h3m'), null);
|
||||
expect(parseTime('blah'), null);
|
||||
expect(parseTime('10a'), null);
|
||||
expect(parseTime('2h'), 120);
|
||||
});
|
||||
|
||||
test('formatTime should format time minutes to formatted string', () {
|
||||
expect(formatTime(5), "5m");
|
||||
expect(formatTime(75), "1h 15m");
|
||||
expect(formatTime(120), "2h");
|
||||
expect(formatTime(-50), "");
|
||||
expect(formatTime(0), "0m");
|
||||
});
|
||||
}
|
@ -30,7 +30,6 @@
|
||||
"passwordHint": "Password",
|
||||
"repeatPasswordHint": "Repeat password",
|
||||
"signUpWith": "Sign up with:"
|
||||
|
||||
},
|
||||
"signIn": {
|
||||
"loginTitle": "Login to @:appName",
|
||||
@ -1012,6 +1011,7 @@
|
||||
"checklistFieldName": "Checklist",
|
||||
"relationFieldName": "Relation",
|
||||
"summaryFieldName": "AI Summary",
|
||||
"timeFieldName": "Time",
|
||||
"translateFieldName": "AI Translate",
|
||||
"translateTo": "Translate to",
|
||||
"numberFormat": "Number format",
|
||||
@ -1915,4 +1915,3 @@
|
||||
"title": "Spaces"
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -657,6 +657,12 @@ impl<'a> TestRowBuilder<'a> {
|
||||
checklist_field.id.clone()
|
||||
}
|
||||
|
||||
pub fn insert_time_cell(&mut self, time: i64) -> String {
|
||||
let time_field = self.field_with_type(&FieldType::Time);
|
||||
self.cell_build.insert_number_cell(&time_field.id, time);
|
||||
time_field.id.clone()
|
||||
}
|
||||
|
||||
pub fn field_with_type(&self, field_type: &FieldType) -> Field {
|
||||
self
|
||||
.fields
|
||||
|
@ -450,6 +450,7 @@ pub enum FieldType {
|
||||
Relation = 10,
|
||||
Summary = 11,
|
||||
Translate = 12,
|
||||
Time = 13,
|
||||
}
|
||||
|
||||
impl Display for FieldType {
|
||||
@ -491,6 +492,7 @@ impl FieldType {
|
||||
FieldType::Relation => "Relation",
|
||||
FieldType::Summary => "Summarize",
|
||||
FieldType::Translate => "Translate",
|
||||
FieldType::Time => "Time",
|
||||
};
|
||||
s.to_string()
|
||||
}
|
||||
@ -547,6 +549,10 @@ impl FieldType {
|
||||
matches!(self, FieldType::Relation)
|
||||
}
|
||||
|
||||
pub fn is_time(&self) -> bool {
|
||||
matches!(self, FieldType::Time)
|
||||
}
|
||||
|
||||
pub fn can_be_group(&self) -> bool {
|
||||
self.is_select_option() || self.is_checkbox() || self.is_url()
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ mod number_filter;
|
||||
mod relation_filter;
|
||||
mod select_option_filter;
|
||||
mod text_filter;
|
||||
mod time_filter;
|
||||
mod util;
|
||||
|
||||
pub use checkbox_filter::*;
|
||||
@ -16,4 +17,5 @@ pub use number_filter::*;
|
||||
pub use relation_filter::*;
|
||||
pub use select_option_filter::*;
|
||||
pub use text_filter::*;
|
||||
pub use time_filter::*;
|
||||
pub use util::*;
|
||||
|
@ -0,0 +1,23 @@
|
||||
use flowy_derive::ProtoBuf;
|
||||
|
||||
use crate::entities::NumberFilterConditionPB;
|
||||
use crate::services::filter::ParseFilterData;
|
||||
|
||||
#[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)]
|
||||
pub struct TimeFilterPB {
|
||||
#[pb(index = 1)]
|
||||
pub condition: NumberFilterConditionPB,
|
||||
|
||||
#[pb(index = 2)]
|
||||
pub content: String,
|
||||
}
|
||||
|
||||
impl ParseFilterData for TimeFilterPB {
|
||||
fn parse(condition: u8, content: String) -> Self {
|
||||
TimeFilterPB {
|
||||
condition: NumberFilterConditionPB::try_from(condition)
|
||||
.unwrap_or(NumberFilterConditionPB::Equal),
|
||||
content,
|
||||
}
|
||||
}
|
||||
}
|
@ -10,7 +10,7 @@ use validator::Validate;
|
||||
|
||||
use crate::entities::{
|
||||
CheckboxFilterPB, ChecklistFilterPB, DateFilterPB, FieldType, NumberFilterPB, RelationFilterPB,
|
||||
SelectOptionFilterPB, TextFilterPB,
|
||||
SelectOptionFilterPB, TextFilterPB, TimeFilterPB,
|
||||
};
|
||||
use crate::services::filter::{Filter, FilterChangeset, FilterInner};
|
||||
|
||||
@ -109,6 +109,10 @@ impl From<&Filter> for FilterPB {
|
||||
.cloned::<TextFilterPB>()
|
||||
.unwrap()
|
||||
.try_into(),
|
||||
FieldType::Time => condition_and_content
|
||||
.cloned::<TimeFilterPB>()
|
||||
.unwrap()
|
||||
.try_into(),
|
||||
FieldType::Translate => condition_and_content
|
||||
.cloned::<TextFilterPB>()
|
||||
.unwrap()
|
||||
@ -160,6 +164,9 @@ impl TryFrom<FilterDataPB> for FilterInner {
|
||||
FieldType::Summary => {
|
||||
BoxAny::new(TextFilterPB::try_from(bytes).map_err(|_| ErrorCode::ProtobufSerde)?)
|
||||
},
|
||||
FieldType::Time => {
|
||||
BoxAny::new(TimeFilterPB::try_from(bytes).map_err(|_| ErrorCode::ProtobufSerde)?)
|
||||
},
|
||||
FieldType::Translate => {
|
||||
BoxAny::new(TextFilterPB::try_from(bytes).map_err(|_| ErrorCode::ProtobufSerde)?)
|
||||
},
|
||||
|
@ -17,6 +17,7 @@ macro_rules! impl_into_field_type {
|
||||
10 => FieldType::Relation,
|
||||
11 => FieldType::Summary,
|
||||
12 => FieldType::Translate,
|
||||
13 => FieldType::Time,
|
||||
_ => {
|
||||
tracing::error!("🔴Can't parse FieldType from value: {}", ty);
|
||||
FieldType::RichText
|
||||
|
@ -6,6 +6,7 @@ mod relation_entities;
|
||||
mod select_option_entities;
|
||||
mod summary_entities;
|
||||
mod text_entities;
|
||||
mod time_entities;
|
||||
mod timestamp_entities;
|
||||
mod translate_entities;
|
||||
mod url_entities;
|
||||
@ -18,6 +19,7 @@ pub use relation_entities::*;
|
||||
pub use select_option_entities::*;
|
||||
pub use summary_entities::*;
|
||||
pub use text_entities::*;
|
||||
pub use time_entities::*;
|
||||
pub use timestamp_entities::*;
|
||||
pub use translate_entities::*;
|
||||
pub use url_entities::*;
|
||||
|
@ -0,0 +1,28 @@
|
||||
use crate::services::field::TimeTypeOption;
|
||||
use flowy_derive::ProtoBuf;
|
||||
|
||||
#[derive(Clone, Debug, Default, ProtoBuf)]
|
||||
pub struct TimeTypeOptionPB {
|
||||
#[pb(index = 1)]
|
||||
pub dummy: String,
|
||||
}
|
||||
|
||||
impl From<TimeTypeOption> for TimeTypeOptionPB {
|
||||
fn from(_data: TimeTypeOption) -> Self {
|
||||
Self {
|
||||
dummy: "".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<TimeTypeOptionPB> for TimeTypeOption {
|
||||
fn from(_data: TimeTypeOptionPB) -> Self {
|
||||
Self
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, ProtoBuf)]
|
||||
pub struct TimeCellDataPB {
|
||||
#[pb(index = 2)]
|
||||
pub time: i64,
|
||||
}
|
@ -222,7 +222,7 @@ impl<'a> CellBuilder<'a> {
|
||||
FieldType::RichText => {
|
||||
cells.insert(field_id, insert_text_cell(cell_str, field));
|
||||
},
|
||||
FieldType::Number => {
|
||||
FieldType::Number | FieldType::Time => {
|
||||
if let Ok(num) = cell_str.parse::<i64>() {
|
||||
cells.insert(field_id, insert_number_cell(num, field));
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ pub mod relation_type_option;
|
||||
pub mod selection_type_option;
|
||||
pub mod summary_type_option;
|
||||
pub mod text_type_option;
|
||||
pub mod time_type_option;
|
||||
pub mod timestamp_type_option;
|
||||
pub mod translate_type_option;
|
||||
mod type_option;
|
||||
@ -20,6 +21,7 @@ pub use number_type_option::*;
|
||||
pub use relation_type_option::*;
|
||||
pub use selection_type_option::*;
|
||||
pub use text_type_option::*;
|
||||
pub use time_type_option::*;
|
||||
pub use timestamp_type_option::*;
|
||||
pub use type_option::*;
|
||||
pub use type_option_cell::*;
|
||||
|
@ -79,13 +79,14 @@ impl CellDataDecoder for RichTextTypeOption {
|
||||
| FieldType::SingleSelect
|
||||
| FieldType::MultiSelect
|
||||
| FieldType::Checkbox
|
||||
| FieldType::URL => Some(StringCellData::from(stringify_cell(cell, field))),
|
||||
| FieldType::URL
|
||||
| FieldType::Summary
|
||||
| FieldType::Translate
|
||||
| FieldType::Time => Some(StringCellData::from(stringify_cell(cell, field))),
|
||||
FieldType::Checklist
|
||||
| FieldType::LastEditedTime
|
||||
| FieldType::CreatedTime
|
||||
| FieldType::Relation => None,
|
||||
FieldType::Summary => Some(StringCellData::from(stringify_cell(cell, field))),
|
||||
FieldType::Translate => Some(StringCellData::from(stringify_cell(cell, field))),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,6 @@
|
||||
mod time;
|
||||
mod time_entities;
|
||||
mod time_filter;
|
||||
|
||||
pub use time::*;
|
||||
pub use time_entities::*;
|
@ -0,0 +1,115 @@
|
||||
use crate::entities::{TimeCellDataPB, TimeFilterPB};
|
||||
use crate::services::cell::{CellDataChangeset, CellDataDecoder};
|
||||
use crate::services::field::{
|
||||
TimeCellData, TypeOption, TypeOptionCellDataCompare, TypeOptionCellDataFilter,
|
||||
TypeOptionCellDataSerde, TypeOptionTransform,
|
||||
};
|
||||
use crate::services::sort::SortCondition;
|
||||
use collab_database::fields::{TypeOptionData, TypeOptionDataBuilder};
|
||||
use collab_database::rows::Cell;
|
||||
use flowy_error::FlowyResult;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::cmp::Ordering;
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, Default)]
|
||||
pub struct TimeTypeOption;
|
||||
|
||||
impl TypeOption for TimeTypeOption {
|
||||
type CellData = TimeCellData;
|
||||
type CellChangeset = TimeCellChangeset;
|
||||
type CellProtobufType = TimeCellDataPB;
|
||||
type CellFilter = TimeFilterPB;
|
||||
}
|
||||
|
||||
impl From<TypeOptionData> for TimeTypeOption {
|
||||
fn from(_data: TypeOptionData) -> Self {
|
||||
Self
|
||||
}
|
||||
}
|
||||
|
||||
impl From<TimeTypeOption> for TypeOptionData {
|
||||
fn from(_data: TimeTypeOption) -> Self {
|
||||
TypeOptionDataBuilder::new().build()
|
||||
}
|
||||
}
|
||||
|
||||
impl TypeOptionCellDataSerde for TimeTypeOption {
|
||||
fn protobuf_encode(
|
||||
&self,
|
||||
cell_data: <Self as TypeOption>::CellData,
|
||||
) -> <Self as TypeOption>::CellProtobufType {
|
||||
if let Some(time) = cell_data.0 {
|
||||
return TimeCellDataPB { time };
|
||||
}
|
||||
TimeCellDataPB {
|
||||
time: i64::default(),
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_cell(&self, cell: &Cell) -> FlowyResult<<Self as TypeOption>::CellData> {
|
||||
Ok(TimeCellData::from(cell))
|
||||
}
|
||||
}
|
||||
|
||||
impl TimeTypeOption {
|
||||
pub fn new() -> Self {
|
||||
Self
|
||||
}
|
||||
}
|
||||
|
||||
impl TypeOptionTransform for TimeTypeOption {}
|
||||
|
||||
impl CellDataDecoder for TimeTypeOption {
|
||||
fn decode_cell(&self, cell: &Cell) -> FlowyResult<<Self as TypeOption>::CellData> {
|
||||
self.parse_cell(cell)
|
||||
}
|
||||
|
||||
fn stringify_cell_data(&self, cell_data: <Self as TypeOption>::CellData) -> String {
|
||||
if let Some(time) = cell_data.0 {
|
||||
return time.to_string();
|
||||
}
|
||||
"".to_string()
|
||||
}
|
||||
|
||||
fn numeric_cell(&self, cell: &Cell) -> Option<f64> {
|
||||
let time_cell_data = self.parse_cell(cell).ok()?;
|
||||
Some(time_cell_data.0.unwrap() as f64)
|
||||
}
|
||||
}
|
||||
|
||||
pub type TimeCellChangeset = String;
|
||||
|
||||
impl CellDataChangeset for TimeTypeOption {
|
||||
fn apply_changeset(
|
||||
&self,
|
||||
changeset: <Self as TypeOption>::CellChangeset,
|
||||
_cell: Option<Cell>,
|
||||
) -> FlowyResult<(Cell, <Self as TypeOption>::CellData)> {
|
||||
let str = changeset.trim().to_string();
|
||||
let cell_data = TimeCellData(str.parse::<i64>().ok());
|
||||
|
||||
Ok((Cell::from(&cell_data), cell_data))
|
||||
}
|
||||
}
|
||||
|
||||
impl TypeOptionCellDataFilter for TimeTypeOption {
|
||||
fn apply_filter(
|
||||
&self,
|
||||
filter: &<Self as TypeOption>::CellFilter,
|
||||
cell_data: &<Self as TypeOption>::CellData,
|
||||
) -> bool {
|
||||
filter.is_visible(cell_data.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl TypeOptionCellDataCompare for TimeTypeOption {
|
||||
fn apply_cmp(
|
||||
&self,
|
||||
cell_data: &<Self as TypeOption>::CellData,
|
||||
other_cell_data: &<Self as TypeOption>::CellData,
|
||||
sort_condition: SortCondition,
|
||||
) -> Ordering {
|
||||
let order = cell_data.0.cmp(&other_cell_data.0);
|
||||
sort_condition.evaluate_order(order)
|
||||
}
|
||||
}
|
@ -0,0 +1,47 @@
|
||||
use crate::entities::FieldType;
|
||||
use crate::services::field::{TypeOptionCellData, CELL_DATA};
|
||||
use collab::core::any_map::AnyMapExtension;
|
||||
use collab_database::rows::{new_cell_builder, Cell};
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct TimeCellData(pub Option<i64>);
|
||||
|
||||
impl TypeOptionCellData for TimeCellData {
|
||||
fn is_cell_empty(&self) -> bool {
|
||||
self.0.is_none()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&Cell> for TimeCellData {
|
||||
fn from(cell: &Cell) -> Self {
|
||||
Self(
|
||||
cell
|
||||
.get_str_value(CELL_DATA)
|
||||
.and_then(|data| data.parse::<i64>().ok()),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::convert::From<String> for TimeCellData {
|
||||
fn from(s: String) -> Self {
|
||||
Self(s.trim().to_string().parse::<i64>().ok())
|
||||
}
|
||||
}
|
||||
|
||||
impl ToString for TimeCellData {
|
||||
fn to_string(&self) -> String {
|
||||
if let Some(time) = self.0 {
|
||||
time.to_string()
|
||||
} else {
|
||||
"".to_string()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&TimeCellData> for Cell {
|
||||
fn from(data: &TimeCellData) -> Self {
|
||||
new_cell_builder(FieldType::Time)
|
||||
.insert_str_value(CELL_DATA, data.to_string())
|
||||
.build()
|
||||
}
|
||||
}
|
@ -0,0 +1,72 @@
|
||||
use collab_database::fields::Field;
|
||||
use collab_database::rows::Cell;
|
||||
|
||||
use crate::entities::{NumberFilterConditionPB, TimeFilterPB};
|
||||
use crate::services::cell::insert_text_cell;
|
||||
use crate::services::filter::PreFillCellsWithFilter;
|
||||
|
||||
impl TimeFilterPB {
|
||||
pub fn is_visible(&self, cell_time: Option<i64>) -> bool {
|
||||
if self.content.is_empty() {
|
||||
match self.condition {
|
||||
NumberFilterConditionPB::NumberIsEmpty => {
|
||||
return cell_time.is_none();
|
||||
},
|
||||
NumberFilterConditionPB::NumberIsNotEmpty => {
|
||||
return cell_time.is_some();
|
||||
},
|
||||
_ => {},
|
||||
}
|
||||
}
|
||||
|
||||
if cell_time.is_none() {
|
||||
return false;
|
||||
}
|
||||
|
||||
let time = cell_time.unwrap();
|
||||
let content_time = self.content.parse::<i64>().unwrap_or_default();
|
||||
match self.condition {
|
||||
NumberFilterConditionPB::Equal => time == content_time,
|
||||
NumberFilterConditionPB::NotEqual => time != content_time,
|
||||
NumberFilterConditionPB::GreaterThan => time > content_time,
|
||||
NumberFilterConditionPB::LessThan => time < content_time,
|
||||
NumberFilterConditionPB::GreaterThanOrEqualTo => time >= content_time,
|
||||
NumberFilterConditionPB::LessThanOrEqualTo => time <= content_time,
|
||||
_ => true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PreFillCellsWithFilter for TimeFilterPB {
|
||||
fn get_compliant_cell(&self, field: &Field) -> (Option<Cell>, bool) {
|
||||
let expected_decimal = || self.content.parse::<i64>().ok();
|
||||
|
||||
let text = match self.condition {
|
||||
NumberFilterConditionPB::Equal
|
||||
| NumberFilterConditionPB::GreaterThanOrEqualTo
|
||||
| NumberFilterConditionPB::LessThanOrEqualTo
|
||||
if !self.content.is_empty() =>
|
||||
{
|
||||
Some(self.content.clone())
|
||||
},
|
||||
NumberFilterConditionPB::GreaterThan if !self.content.is_empty() => {
|
||||
expected_decimal().map(|value| {
|
||||
let answer = value + 1;
|
||||
answer.to_string()
|
||||
})
|
||||
},
|
||||
NumberFilterConditionPB::LessThan if !self.content.is_empty() => {
|
||||
expected_decimal().map(|value| {
|
||||
let answer = value - 1;
|
||||
answer.to_string()
|
||||
})
|
||||
},
|
||||
_ => None,
|
||||
};
|
||||
|
||||
let open_after_create = matches!(self.condition, NumberFilterConditionPB::NumberIsNotEmpty);
|
||||
|
||||
// use `insert_text_cell` because self.content might not be a parsable i64.
|
||||
(text.map(|s| insert_text_cell(s, field)), open_after_create)
|
||||
}
|
||||
}
|
@ -11,7 +11,7 @@ use flowy_error::FlowyResult;
|
||||
use crate::entities::{
|
||||
CheckboxTypeOptionPB, ChecklistTypeOptionPB, DateTypeOptionPB, FieldType,
|
||||
MultiSelectTypeOptionPB, NumberTypeOptionPB, RelationTypeOptionPB, RichTextTypeOptionPB,
|
||||
SingleSelectTypeOptionPB, SummarizationTypeOptionPB, TimestampTypeOptionPB,
|
||||
SingleSelectTypeOptionPB, SummarizationTypeOptionPB, TimeTypeOptionPB, TimestampTypeOptionPB,
|
||||
TranslateTypeOptionPB, URLTypeOptionPB,
|
||||
};
|
||||
use crate::services::cell::CellDataDecoder;
|
||||
@ -20,7 +20,7 @@ use crate::services::field::summary_type_option::summary::SummarizationTypeOptio
|
||||
use crate::services::field::translate_type_option::translate::TranslateTypeOption;
|
||||
use crate::services::field::{
|
||||
CheckboxTypeOption, DateTypeOption, MultiSelectTypeOption, NumberTypeOption, RelationTypeOption,
|
||||
RichTextTypeOption, SingleSelectTypeOption, TimestampTypeOption, URLTypeOption,
|
||||
RichTextTypeOption, SingleSelectTypeOption, TimeTypeOption, TimestampTypeOption, URLTypeOption,
|
||||
};
|
||||
use crate::services::filter::{ParseFilterData, PreFillCellsWithFilter};
|
||||
use crate::services::sort::SortCondition;
|
||||
@ -187,6 +187,7 @@ pub fn type_option_data_from_pb<T: Into<Bytes>>(
|
||||
FieldType::Summary => {
|
||||
SummarizationTypeOptionPB::try_from(bytes).map(|pb| SummarizationTypeOption::from(pb).into())
|
||||
},
|
||||
FieldType::Time => TimeTypeOptionPB::try_from(bytes).map(|pb| TimeTypeOption::from(pb).into()),
|
||||
FieldType::Translate => {
|
||||
TranslateTypeOptionPB::try_from(bytes).map(|pb| TranslateTypeOption::from(pb).into())
|
||||
},
|
||||
@ -257,6 +258,10 @@ pub fn type_option_to_pb(type_option: TypeOptionData, field_type: &FieldType) ->
|
||||
.try_into()
|
||||
.unwrap()
|
||||
},
|
||||
FieldType::Time => {
|
||||
let time_type_option: TimeTypeOption = type_option.into();
|
||||
TimeTypeOptionPB::from(time_type_option).try_into().unwrap()
|
||||
},
|
||||
FieldType::Translate => {
|
||||
let translate_type_option: TranslateTypeOption = type_option.into();
|
||||
TranslateTypeOptionPB::from(translate_type_option)
|
||||
@ -284,5 +289,6 @@ pub fn default_type_option_data_from_type(field_type: FieldType) -> TypeOptionDa
|
||||
FieldType::Relation => RelationTypeOption::default().into(),
|
||||
FieldType::Summary => SummarizationTypeOption::default().into(),
|
||||
FieldType::Translate => TranslateTypeOption::default().into(),
|
||||
FieldType::Time => TimeTypeOption.into(),
|
||||
}
|
||||
}
|
||||
|
@ -14,9 +14,9 @@ use crate::services::field::summary_type_option::summary::SummarizationTypeOptio
|
||||
use crate::services::field::translate_type_option::translate::TranslateTypeOption;
|
||||
use crate::services::field::{
|
||||
CheckboxTypeOption, ChecklistTypeOption, DateTypeOption, MultiSelectTypeOption, NumberTypeOption,
|
||||
RelationTypeOption, RichTextTypeOption, SingleSelectTypeOption, TimestampTypeOption, TypeOption,
|
||||
TypeOptionCellData, TypeOptionCellDataCompare, TypeOptionCellDataFilter, TypeOptionCellDataSerde,
|
||||
TypeOptionTransform, URLTypeOption,
|
||||
RelationTypeOption, RichTextTypeOption, SingleSelectTypeOption, TimeTypeOption,
|
||||
TimestampTypeOption, TypeOption, TypeOptionCellData, TypeOptionCellDataCompare,
|
||||
TypeOptionCellDataFilter, TypeOptionCellDataSerde, TypeOptionTransform, URLTypeOption,
|
||||
};
|
||||
use crate::services::sort::SortCondition;
|
||||
|
||||
@ -450,6 +450,16 @@ impl<'a> TypeOptionCellExt<'a> {
|
||||
self.cell_data_cache.clone(),
|
||||
)
|
||||
}),
|
||||
FieldType::Time => self
|
||||
.field
|
||||
.get_type_option::<TimeTypeOption>(field_type)
|
||||
.map(|type_option| {
|
||||
TypeOptionCellDataHandlerImpl::new_with_boxed(
|
||||
type_option,
|
||||
field_type,
|
||||
self.cell_data_cache.clone(),
|
||||
)
|
||||
}),
|
||||
FieldType::Translate => self
|
||||
.field
|
||||
.get_type_option::<TranslateTypeOption>(field_type)
|
||||
@ -563,6 +573,9 @@ fn get_type_option_transform_handler(
|
||||
},
|
||||
FieldType::Summary => Box::new(SummarizationTypeOption::from(type_option_data))
|
||||
as Box<dyn TypeOptionTransformHandler>,
|
||||
FieldType::Time => {
|
||||
Box::new(TimeTypeOption::from(type_option_data)) as Box<dyn TypeOptionTransformHandler>
|
||||
},
|
||||
FieldType::Translate => {
|
||||
Box::new(TranslateTypeOption::from(type_option_data)) as Box<dyn TypeOptionTransformHandler>
|
||||
},
|
||||
|
@ -303,6 +303,10 @@ impl FilterController {
|
||||
let filter = condition_and_content.cloned::<ChecklistFilterPB>().unwrap();
|
||||
filter.get_compliant_cell(field)
|
||||
},
|
||||
FieldType::Time => {
|
||||
let filter = condition_and_content.cloned::<TimeFilterPB>().unwrap();
|
||||
filter.get_compliant_cell(field)
|
||||
},
|
||||
_ => (None, false),
|
||||
};
|
||||
|
||||
|
@ -12,6 +12,7 @@ use lib_infra::box_any::BoxAny;
|
||||
use crate::entities::{
|
||||
CheckboxFilterPB, ChecklistFilterPB, DateFilterContent, DateFilterPB, FieldType, FilterType,
|
||||
InsertedRowPB, NumberFilterPB, RelationFilterPB, SelectOptionFilterPB, TextFilterPB,
|
||||
TimeFilterPB,
|
||||
};
|
||||
use crate::services::field::SelectOptionIds;
|
||||
|
||||
@ -282,6 +283,7 @@ impl FilterInner {
|
||||
FieldType::Relation => BoxAny::new(RelationFilterPB::parse(condition as u8, content)),
|
||||
FieldType::Summary => BoxAny::new(TextFilterPB::parse(condition as u8, content)),
|
||||
FieldType::Translate => BoxAny::new(TextFilterPB::parse(condition as u8, content)),
|
||||
FieldType::Time => BoxAny::new(TimeFilterPB::parse(condition as u8, content)),
|
||||
};
|
||||
|
||||
FilterInner::Data {
|
||||
@ -368,6 +370,10 @@ impl<'a> From<&'a Filter> for FilterMap {
|
||||
let filter = condition_and_content.cloned::<TextFilterPB>()?;
|
||||
(filter.condition as u8, filter.content)
|
||||
},
|
||||
FieldType::Time => {
|
||||
let filter = condition_and_content.cloned::<TimeFilterPB>()?;
|
||||
(filter.condition as u8, filter.content)
|
||||
},
|
||||
FieldType::Translate => {
|
||||
let filter = condition_and_content.cloned::<TextFilterPB>()?;
|
||||
(filter.condition as u8, filter.content)
|
||||
|
@ -4,7 +4,7 @@ use flowy_database2::entities::FieldType;
|
||||
use flowy_database2::services::field::{
|
||||
ChecklistCellChangeset, DateCellChangeset, DateCellData, MultiSelectTypeOption,
|
||||
RelationCellChangeset, SelectOptionCellChangeset, SingleSelectTypeOption, StringCellData,
|
||||
URLCellData,
|
||||
TimeCellData, URLCellData,
|
||||
};
|
||||
use lib_infra::box_any::BoxAny;
|
||||
|
||||
@ -200,3 +200,20 @@ async fn update_updated_at_field_on_other_cell_update() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn time_cell_data_test() {
|
||||
let test = DatabaseCellTest::new().await;
|
||||
let time_field = test.get_first_field(FieldType::Time);
|
||||
let cells = test
|
||||
.editor
|
||||
.get_cells_for_field(&test.view_id, &time_field.id)
|
||||
.await;
|
||||
|
||||
if let Some(cell) = cells[0].cell.as_ref() {
|
||||
let cell = TimeCellData::from(cell);
|
||||
|
||||
assert!(cell.0.is_some());
|
||||
assert_eq!(cell.0.unwrap_or_default(), 75);
|
||||
}
|
||||
}
|
||||
|
@ -40,6 +40,26 @@ async fn grid_create_field() {
|
||||
},
|
||||
];
|
||||
test.run_scripts(scripts).await;
|
||||
|
||||
let (params, field) = create_time_field(&test.view_id());
|
||||
let scripts = vec![
|
||||
CreateField { params },
|
||||
AssertFieldTypeOptionEqual {
|
||||
field_index: test.field_count(),
|
||||
expected_type_option_data: field.get_any_type_option(field.field_type).unwrap(),
|
||||
},
|
||||
];
|
||||
test.run_scripts(scripts).await;
|
||||
|
||||
let (params, field) = create_time_field(&test.view_id());
|
||||
let scripts = vec![
|
||||
CreateField { params },
|
||||
AssertFieldTypeOptionEqual {
|
||||
field_index: test.field_count(),
|
||||
expected_type_option_data: field.get_any_type_option(field.field_type).unwrap(),
|
||||
},
|
||||
];
|
||||
test.run_scripts(scripts).await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
@ -4,7 +4,7 @@ use collab_database::views::OrderObjectPosition;
|
||||
use flowy_database2::entities::{CreateFieldParams, FieldType};
|
||||
use flowy_database2::services::field::{
|
||||
type_option_to_pb, DateFormat, DateTypeOption, FieldBuilder, RichTextTypeOption, SelectOption,
|
||||
SingleSelectTypeOption, TimeFormat, TimestampTypeOption,
|
||||
SingleSelectTypeOption, TimeFormat, TimeTypeOption, TimestampTypeOption,
|
||||
};
|
||||
|
||||
pub fn create_text_field(grid_id: &str) -> (CreateFieldParams, Field) {
|
||||
@ -98,3 +98,21 @@ pub fn create_timestamp_field(grid_id: &str, field_type: FieldType) -> (CreateFi
|
||||
};
|
||||
(params, field)
|
||||
}
|
||||
|
||||
pub fn create_time_field(grid_id: &str) -> (CreateFieldParams, Field) {
|
||||
let field_type = FieldType::Time;
|
||||
let type_option = TimeTypeOption;
|
||||
let text_field = FieldBuilder::new(field_type, type_option.clone())
|
||||
.name("Time field")
|
||||
.build();
|
||||
|
||||
let type_option_data = type_option_to_pb(type_option.into(), &field_type).to_vec();
|
||||
let params = CreateFieldParams {
|
||||
view_id: grid_id.to_owned(),
|
||||
field_type,
|
||||
type_option_data: Some(type_option_data),
|
||||
field_name: None,
|
||||
position: OrderObjectPosition::default(),
|
||||
};
|
||||
(params, text_field)
|
||||
}
|
||||
|
@ -6,3 +6,4 @@ mod number_filter_test;
|
||||
mod script;
|
||||
mod select_option_filter_test;
|
||||
mod text_filter_test;
|
||||
mod time_filter_test;
|
||||
|
@ -0,0 +1,121 @@
|
||||
use flowy_database2::entities::{FieldType, NumberFilterConditionPB, TimeFilterPB};
|
||||
use lib_infra::box_any::BoxAny;
|
||||
|
||||
use crate::database::filter_test::script::FilterScript::*;
|
||||
use crate::database::filter_test::script::{DatabaseFilterTest, FilterRowChanged};
|
||||
|
||||
#[tokio::test]
|
||||
async fn grid_filter_time_is_equal_test() {
|
||||
let mut test = DatabaseFilterTest::new().await;
|
||||
let row_count = test.row_details.len();
|
||||
let expected = 1;
|
||||
let scripts = vec![
|
||||
CreateDataFilter {
|
||||
parent_filter_id: None,
|
||||
field_type: FieldType::Time,
|
||||
data: BoxAny::new(TimeFilterPB {
|
||||
condition: NumberFilterConditionPB::Equal,
|
||||
content: "75".to_string(),
|
||||
}),
|
||||
changed: Some(FilterRowChanged {
|
||||
showing_num_of_rows: 0,
|
||||
hiding_num_of_rows: row_count - expected,
|
||||
}),
|
||||
},
|
||||
AssertNumberOfVisibleRows { expected },
|
||||
];
|
||||
test.run_scripts(scripts).await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn grid_filter_time_is_less_than_test() {
|
||||
let mut test = DatabaseFilterTest::new().await;
|
||||
let row_count = test.row_details.len();
|
||||
let expected = 1;
|
||||
let scripts = vec![
|
||||
CreateDataFilter {
|
||||
parent_filter_id: None,
|
||||
field_type: FieldType::Time,
|
||||
|
||||
data: BoxAny::new(TimeFilterPB {
|
||||
condition: NumberFilterConditionPB::LessThan,
|
||||
content: "80".to_string(),
|
||||
}),
|
||||
changed: Some(FilterRowChanged {
|
||||
showing_num_of_rows: 0,
|
||||
hiding_num_of_rows: row_count - expected,
|
||||
}),
|
||||
},
|
||||
AssertNumberOfVisibleRows { expected },
|
||||
];
|
||||
test.run_scripts(scripts).await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn grid_filter_time_is_less_than_or_equal_test() {
|
||||
let mut test = DatabaseFilterTest::new().await;
|
||||
let row_count = test.row_details.len();
|
||||
let expected = 1;
|
||||
let scripts = vec![
|
||||
CreateDataFilter {
|
||||
parent_filter_id: None,
|
||||
field_type: FieldType::Time,
|
||||
data: BoxAny::new(TimeFilterPB {
|
||||
condition: NumberFilterConditionPB::LessThanOrEqualTo,
|
||||
content: "75".to_string(),
|
||||
}),
|
||||
changed: Some(FilterRowChanged {
|
||||
showing_num_of_rows: 0,
|
||||
hiding_num_of_rows: row_count - expected,
|
||||
}),
|
||||
},
|
||||
AssertNumberOfVisibleRows { expected },
|
||||
];
|
||||
test.run_scripts(scripts).await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn grid_filter_time_is_empty_test() {
|
||||
let mut test = DatabaseFilterTest::new().await;
|
||||
let row_count = test.row_details.len();
|
||||
let expected = 6;
|
||||
let scripts = vec![
|
||||
CreateDataFilter {
|
||||
parent_filter_id: None,
|
||||
field_type: FieldType::Time,
|
||||
data: BoxAny::new(TimeFilterPB {
|
||||
condition: NumberFilterConditionPB::NumberIsEmpty,
|
||||
content: "".to_string(),
|
||||
}),
|
||||
changed: Some(FilterRowChanged {
|
||||
showing_num_of_rows: 0,
|
||||
hiding_num_of_rows: row_count - expected,
|
||||
}),
|
||||
},
|
||||
AssertNumberOfVisibleRows { expected },
|
||||
];
|
||||
test.run_scripts(scripts).await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn grid_filter_time_is_not_empty_test() {
|
||||
let mut test = DatabaseFilterTest::new().await;
|
||||
let row_count = test.row_details.len();
|
||||
let expected = 1;
|
||||
let scripts = vec![
|
||||
CreateDataFilter {
|
||||
parent_filter_id: None,
|
||||
field_type: FieldType::Time,
|
||||
data: BoxAny::new(TimeFilterPB {
|
||||
condition: NumberFilterConditionPB::NumberIsNotEmpty,
|
||||
content: "".to_string(),
|
||||
}),
|
||||
changed: Some(FilterRowChanged {
|
||||
showing_num_of_rows: 0,
|
||||
hiding_num_of_rows: row_count - expected,
|
||||
}),
|
||||
},
|
||||
AssertNumberOfVisibleRows { expected },
|
||||
];
|
||||
test.run_scripts(scripts).await;
|
||||
}
|
@ -134,6 +134,12 @@ pub fn make_test_board() -> DatabaseData {
|
||||
.build();
|
||||
fields.push(relation_field);
|
||||
},
|
||||
FieldType::Time => {
|
||||
let time_field = FieldBuilder::from_field_type(field_type)
|
||||
.name("Estimated time")
|
||||
.build();
|
||||
fields.push(time_field);
|
||||
},
|
||||
FieldType::Translate => {},
|
||||
}
|
||||
}
|
||||
|
@ -10,7 +10,7 @@ use flowy_database2::services::field::translate_type_option::translate::Translat
|
||||
use flowy_database2::services::field::{
|
||||
ChecklistTypeOption, DateFormat, DateTypeOption, FieldBuilder, MultiSelectTypeOption,
|
||||
NumberFormat, NumberTypeOption, RelationTypeOption, SelectOption, SelectOptionColor,
|
||||
SingleSelectTypeOption, TimeFormat, TimestampTypeOption,
|
||||
SingleSelectTypeOption, TimeFormat, TimeTypeOption, TimestampTypeOption,
|
||||
};
|
||||
use flowy_database2::services::field_settings::default_field_settings_for_fields;
|
||||
|
||||
@ -133,6 +133,13 @@ pub fn make_test_grid() -> DatabaseData {
|
||||
.build();
|
||||
fields.push(relation_field);
|
||||
},
|
||||
FieldType::Time => {
|
||||
let type_option = TimeTypeOption;
|
||||
let time_field = FieldBuilder::new(field_type, type_option)
|
||||
.name("Estimated time")
|
||||
.build();
|
||||
fields.push(time_field);
|
||||
},
|
||||
FieldType::Translate => {
|
||||
let type_option = TranslateTypeOption {
|
||||
auto_fill: false,
|
||||
@ -168,6 +175,7 @@ pub fn make_test_grid() -> DatabaseData {
|
||||
FieldType::Checklist => {
|
||||
row_builder.insert_checklist_cell(vec![("First thing".to_string(), false)])
|
||||
},
|
||||
FieldType::Time => row_builder.insert_time_cell(75),
|
||||
_ => "".to_owned(),
|
||||
};
|
||||
}
|
||||
|
@ -83,6 +83,7 @@ async fn export_and_then_import_meta_csv_test() {
|
||||
FieldType::CreatedTime => {},
|
||||
FieldType::Relation => {},
|
||||
FieldType::Summary => {},
|
||||
FieldType::Time => {},
|
||||
FieldType::Translate => {},
|
||||
}
|
||||
} else {
|
||||
@ -167,6 +168,7 @@ async fn history_database_import_test() {
|
||||
FieldType::CreatedTime => {},
|
||||
FieldType::Relation => {},
|
||||
FieldType::Summary => {},
|
||||
FieldType::Time => {},
|
||||
FieldType::Translate => {},
|
||||
}
|
||||
} else {
|
||||
|
Loading…
Reference in New Issue
Block a user