mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
feat: add end date to time cell data (#3369)
* feat: add end date to time cell data * feat: implement ui for end time * test: add date to text test * chore: clippy warnings * fix: unexpected time parsing * fix: fix the date logic when toggling end date --------- Co-authored-by: nathan <nathan@appflowy.io>
This commit is contained in:
parent
b700f95c7f
commit
124d435f09
@ -185,7 +185,7 @@ void main() {
|
|||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('edit time cell', (tester) async {
|
testWidgets('edit date time cell', (tester) async {
|
||||||
await tester.initializeAppFlowy();
|
await tester.initializeAppFlowy();
|
||||||
await tester.tapGoButton();
|
await tester.tapGoButton();
|
||||||
|
|
||||||
|
@ -25,6 +25,7 @@ import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/
|
|||||||
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/field_type_list.dart';
|
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/field_type_list.dart';
|
||||||
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/field_type_option_editor.dart';
|
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/field_type_option_editor.dart';
|
||||||
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/type_option/date.dart';
|
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/type_option/date.dart';
|
||||||
|
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/type_option/timestamp.dart';
|
||||||
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/row/row.dart';
|
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/row/row.dart';
|
||||||
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/sort/create_sort_list.dart';
|
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/sort/create_sort_list.dart';
|
||||||
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/sort/order_panel.dart';
|
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/sort/order_panel.dart';
|
||||||
@ -330,7 +331,17 @@ extension AppFlowyDatabaseTest on WidgetTester {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> toggleIncludeTime() async {
|
Future<void> toggleIncludeTime() async {
|
||||||
final findDateEditor = find.byType(DateCellEditor);
|
final findDateEditor = find.byType(IncludeTimeButton);
|
||||||
|
final findToggle = find.byType(Toggle);
|
||||||
|
final finder = find.descendant(
|
||||||
|
of: findDateEditor,
|
||||||
|
matching: findToggle,
|
||||||
|
);
|
||||||
|
await tapButton(finder);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> toggleDateRange() async {
|
||||||
|
final findDateEditor = find.byType(EndTimeButton);
|
||||||
final findToggle = find.byType(Toggle);
|
final findToggle = find.byType(Toggle);
|
||||||
final finder = find.descendant(
|
final finder = find.descendant(
|
||||||
of: findDateEditor,
|
of: findDateEditor,
|
||||||
|
@ -20,11 +20,15 @@ final class DateCellBackendService {
|
|||||||
Future<Either<Unit, FlowyError>> update({
|
Future<Either<Unit, FlowyError>> update({
|
||||||
DateTime? date,
|
DateTime? date,
|
||||||
String? time,
|
String? time,
|
||||||
|
DateTime? endDate,
|
||||||
|
String? endTime,
|
||||||
required includeTime,
|
required includeTime,
|
||||||
|
required isRange,
|
||||||
}) {
|
}) {
|
||||||
final payload = DateChangesetPB.create()
|
final payload = DateChangesetPB.create()
|
||||||
..cellId = cellId
|
..cellId = cellId
|
||||||
..includeTime = includeTime;
|
..includeTime = includeTime
|
||||||
|
..isRange = isRange;
|
||||||
|
|
||||||
if (date != null) {
|
if (date != null) {
|
||||||
final dateTimestamp = date.millisecondsSinceEpoch ~/ 1000;
|
final dateTimestamp = date.millisecondsSinceEpoch ~/ 1000;
|
||||||
@ -33,6 +37,13 @@ final class DateCellBackendService {
|
|||||||
if (time != null) {
|
if (time != null) {
|
||||||
payload.time = time;
|
payload.time = time;
|
||||||
}
|
}
|
||||||
|
if (endDate != null) {
|
||||||
|
final dateTimestamp = endDate.millisecondsSinceEpoch ~/ 1000;
|
||||||
|
payload.endDate = Int64(dateTimestamp);
|
||||||
|
}
|
||||||
|
if (endTime != null) {
|
||||||
|
payload.endTime = endTime;
|
||||||
|
}
|
||||||
|
|
||||||
return DatabaseEventUpdateDateCell(payload).send();
|
return DatabaseEventUpdateDateCell(payload).send();
|
||||||
}
|
}
|
||||||
|
@ -80,11 +80,20 @@ class DateCardCellState with _$DateCardCellState {
|
|||||||
String _dateStrFromCellData(DateCellDataPB? cellData) {
|
String _dateStrFromCellData(DateCellDataPB? cellData) {
|
||||||
String dateStr = "";
|
String dateStr = "";
|
||||||
if (cellData != null) {
|
if (cellData != null) {
|
||||||
|
if (cellData.isRange) {
|
||||||
if (cellData.includeTime) {
|
if (cellData.includeTime) {
|
||||||
dateStr = '${cellData.date} ${cellData.time}';
|
dateStr =
|
||||||
|
"${cellData.date} ${cellData.time} → ${cellData.endDate} ${cellData.endTime}";
|
||||||
|
} else {
|
||||||
|
dateStr = "${cellData.date} → ${cellData.endDate}";
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (cellData.includeTime) {
|
||||||
|
dateStr = "${cellData.date} ${cellData.time}";
|
||||||
} else {
|
} else {
|
||||||
dateStr = cellData.date;
|
dateStr = cellData.date;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return dateStr;
|
return dateStr;
|
||||||
}
|
}
|
||||||
|
@ -10,10 +10,10 @@ import 'package:appflowy_backend/protobuf/flowy-error/code.pb.dart';
|
|||||||
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart'
|
import 'package:easy_localization/easy_localization.dart'
|
||||||
show StringTranslateExtension;
|
show StringTranslateExtension;
|
||||||
|
import 'package:flowy_infra/time/duration.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
import 'package:protobuf/protobuf.dart';
|
import 'package:protobuf/protobuf.dart';
|
||||||
import 'package:table_calendar/table_calendar.dart';
|
|
||||||
|
|
||||||
part 'date_cal_bloc.freezed.dart';
|
part 'date_cal_bloc.freezed.dart';
|
||||||
|
|
||||||
@ -38,40 +38,64 @@ class DateCellCalendarBloc
|
|||||||
await event.when(
|
await event.when(
|
||||||
initial: () async => _startListening(),
|
initial: () async => _startListening(),
|
||||||
didReceiveCellUpdate: (DateCellDataPB? cellData) {
|
didReceiveCellUpdate: (DateCellDataPB? cellData) {
|
||||||
final (dateTime, time, includeTime) =
|
final (dateTime, endDateTime, time, endTime, includeTime, isRange) =
|
||||||
_dateDataFromCellData(cellData);
|
_dateDataFromCellData(cellData);
|
||||||
emit(
|
emit(
|
||||||
state.copyWith(
|
state.copyWith(
|
||||||
dateTime: dateTime,
|
dateTime: dateTime,
|
||||||
time: time,
|
time: time,
|
||||||
|
endDateTime: endDateTime,
|
||||||
|
endTime: endTime,
|
||||||
includeTime: includeTime,
|
includeTime: includeTime,
|
||||||
|
isRange: isRange,
|
||||||
|
startDay: isRange ? dateTime : null,
|
||||||
|
endDay: isRange ? endDateTime : null,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
didReceiveTimeFormatError: (String? timeFormatError) {
|
didReceiveTimeFormatError:
|
||||||
emit(state.copyWith(timeFormatError: timeFormatError));
|
(String? parseTimeError, String? parseEndTimeError) {
|
||||||
|
emit(
|
||||||
|
state.copyWith(
|
||||||
|
parseTimeError: parseTimeError,
|
||||||
|
parseEndTimeError: parseEndTimeError,
|
||||||
|
),
|
||||||
|
);
|
||||||
},
|
},
|
||||||
selectDay: (date) async {
|
selectDay: (date) async {
|
||||||
|
if (state.isRange) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
await _updateDateData(date: date);
|
await _updateDateData(date: date);
|
||||||
},
|
},
|
||||||
setIncludeTime: (includeTime) async {
|
setIncludeTime: (includeTime) async {
|
||||||
await _updateDateData(includeTime: includeTime);
|
await _updateDateData(includeTime: includeTime);
|
||||||
},
|
},
|
||||||
|
setIsRange: (isRange) async {
|
||||||
|
await _updateDateData(isRange: isRange);
|
||||||
|
},
|
||||||
setTime: (time) async {
|
setTime: (time) async {
|
||||||
await _updateDateData(time: time);
|
await _updateDateData(time: time);
|
||||||
},
|
},
|
||||||
|
selectDateRange: (DateTime? start, DateTime? end) async {
|
||||||
|
if (end == null) {
|
||||||
|
emit(state.copyWith(startDay: start, endDay: null));
|
||||||
|
} else {
|
||||||
|
await _updateDateData(
|
||||||
|
date: start!.toLocal().date,
|
||||||
|
endDate: end.toLocal().date,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
setEndTime: (String endTime) async {
|
||||||
|
await _updateDateData(endTime: endTime);
|
||||||
|
},
|
||||||
setDateFormat: (dateFormat) async {
|
setDateFormat: (dateFormat) async {
|
||||||
await _updateTypeOption(emit, dateFormat: dateFormat);
|
await _updateTypeOption(emit, dateFormat: dateFormat);
|
||||||
},
|
},
|
||||||
setTimeFormat: (timeFormat) async {
|
setTimeFormat: (timeFormat) async {
|
||||||
await _updateTypeOption(emit, timeFormat: timeFormat);
|
await _updateTypeOption(emit, timeFormat: timeFormat);
|
||||||
},
|
},
|
||||||
setCalFormat: (format) {
|
|
||||||
emit(state.copyWith(format: format));
|
|
||||||
},
|
|
||||||
setFocusedDay: (focusedDay) {
|
|
||||||
emit(state.copyWith(focusedDay: focusedDay));
|
|
||||||
},
|
|
||||||
clearDate: () async {
|
clearDate: () async {
|
||||||
await _clearDate();
|
await _clearDate();
|
||||||
},
|
},
|
||||||
@ -83,39 +107,66 @@ class DateCellCalendarBloc
|
|||||||
Future<void> _updateDateData({
|
Future<void> _updateDateData({
|
||||||
DateTime? date,
|
DateTime? date,
|
||||||
String? time,
|
String? time,
|
||||||
|
DateTime? endDate,
|
||||||
|
String? endTime,
|
||||||
bool? includeTime,
|
bool? includeTime,
|
||||||
|
bool? isRange,
|
||||||
}) async {
|
}) async {
|
||||||
// make sure that not both date and time are updated at the same time
|
// make sure that not both date and time are updated at the same time
|
||||||
assert(
|
assert(
|
||||||
date == null && time == null ||
|
!(date != null && time != null) || !(endDate != null && endTime != null),
|
||||||
date == null && time != null ||
|
|
||||||
date != null && time == null,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// if not updating the time, use the old time in the state
|
||||||
final String? newTime = time ?? state.time;
|
final String? newTime = time ?? state.time;
|
||||||
DateTime? newDate = _utcToLocalAndAddCurrentTime(date);
|
DateTime? newDate;
|
||||||
if (time != null && time.isNotEmpty) {
|
if (time != null && time.isNotEmpty) {
|
||||||
newDate = state.dateTime ?? DateTime.now();
|
newDate = state.dateTime ?? DateTime.now();
|
||||||
|
} else {
|
||||||
|
newDate = _utcToLocalAndAddCurrentTime(date);
|
||||||
|
}
|
||||||
|
|
||||||
|
// if not updating the time, use the old time in the state
|
||||||
|
final String? newEndTime = endTime ?? state.endTime;
|
||||||
|
DateTime? newEndDate;
|
||||||
|
if (endTime != null && endTime.isNotEmpty) {
|
||||||
|
newEndDate = state.endDateTime ?? DateTime.now();
|
||||||
|
} else {
|
||||||
|
newEndDate = _utcToLocalAndAddCurrentTime(endDate);
|
||||||
}
|
}
|
||||||
|
|
||||||
final result = await _dateCellBackendService.update(
|
final result = await _dateCellBackendService.update(
|
||||||
date: newDate,
|
date: newDate,
|
||||||
time: newTime,
|
time: newTime,
|
||||||
|
endDate: newEndDate,
|
||||||
|
endTime: newEndTime,
|
||||||
includeTime: includeTime ?? state.includeTime,
|
includeTime: includeTime ?? state.includeTime,
|
||||||
|
isRange: isRange ?? state.isRange,
|
||||||
);
|
);
|
||||||
|
|
||||||
result.fold(
|
result.fold(
|
||||||
(_) {
|
(_) {
|
||||||
if (!isClosed && state.timeFormatError != null) {
|
if (!isClosed &&
|
||||||
add(const DateCellCalendarEvent.didReceiveTimeFormatError(null));
|
(state.parseEndTimeError != null || state.parseTimeError != null)) {
|
||||||
|
add(
|
||||||
|
const DateCellCalendarEvent.didReceiveTimeFormatError(null, null),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
(err) {
|
(err) {
|
||||||
switch (err.code) {
|
switch (err.code) {
|
||||||
case ErrorCode.InvalidDateTimeFormat:
|
case ErrorCode.InvalidDateTimeFormat:
|
||||||
if (isClosed) return;
|
if (isClosed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// to determine which textfield should show error
|
||||||
|
final (startError, endError) = newDate != null
|
||||||
|
? (timeFormatPrompt(err), null)
|
||||||
|
: (null, timeFormatPrompt(err));
|
||||||
add(
|
add(
|
||||||
DateCellCalendarEvent.didReceiveTimeFormatError(
|
DateCellCalendarEvent.didReceiveTimeFormatError(
|
||||||
timeFormatPrompt(err),
|
startError,
|
||||||
|
endError,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
@ -130,9 +181,13 @@ class DateCellCalendarBloc
|
|||||||
final result = await _dateCellBackendService.clear();
|
final result = await _dateCellBackendService.clear();
|
||||||
result.fold(
|
result.fold(
|
||||||
(_) {
|
(_) {
|
||||||
if (!isClosed) {
|
if (isClosed) {
|
||||||
add(const DateCellCalendarEvent.didReceiveTimeFormatError(null));
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
add(
|
||||||
|
const DateCellCalendarEvent.didReceiveTimeFormatError(null, null),
|
||||||
|
);
|
||||||
},
|
},
|
||||||
(err) => Log.error(err),
|
(err) => Log.error(err),
|
||||||
);
|
);
|
||||||
@ -157,18 +212,13 @@ class DateCellCalendarBloc
|
|||||||
}
|
}
|
||||||
|
|
||||||
String timeFormatPrompt(FlowyError error) {
|
String timeFormatPrompt(FlowyError error) {
|
||||||
String msg = "${LocaleKeys.grid_field_invalidTimeFormat.tr()}.";
|
return switch (state.dateTypeOptionPB.timeFormat) {
|
||||||
switch (state.dateTypeOptionPB.timeFormat) {
|
TimeFormatPB.TwelveHour =>
|
||||||
case TimeFormatPB.TwelveHour:
|
"${LocaleKeys.grid_field_invalidTimeFormat.tr()}. e.g. 01:00 PM",
|
||||||
msg = "$msg e.g. 01:00 PM";
|
TimeFormatPB.TwentyFourHour =>
|
||||||
break;
|
"${LocaleKeys.grid_field_invalidTimeFormat.tr()}. e.g. 13:00",
|
||||||
case TimeFormatPB.TwentyFourHour:
|
_ => "",
|
||||||
msg = "$msg e.g. 13:00";
|
};
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
return msg;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -235,19 +285,21 @@ class DateCellCalendarEvent with _$DateCellCalendarEvent {
|
|||||||
DateCellDataPB? data,
|
DateCellDataPB? data,
|
||||||
) = _DidReceiveCellUpdate;
|
) = _DidReceiveCellUpdate;
|
||||||
const factory DateCellCalendarEvent.didReceiveTimeFormatError(
|
const factory DateCellCalendarEvent.didReceiveTimeFormatError(
|
||||||
String? timeformatError,
|
String? parseTimeError,
|
||||||
|
String? parseEndTimeError,
|
||||||
) = _DidReceiveTimeFormatError;
|
) = _DidReceiveTimeFormatError;
|
||||||
|
|
||||||
// table calendar's UI settings
|
|
||||||
const factory DateCellCalendarEvent.setFocusedDay(DateTime day) = _FocusedDay;
|
|
||||||
const factory DateCellCalendarEvent.setCalFormat(CalendarFormat format) =
|
|
||||||
_CalendarFormat;
|
|
||||||
|
|
||||||
// date cell data is modified
|
// date cell data is modified
|
||||||
const factory DateCellCalendarEvent.selectDay(DateTime day) = _SelectDay;
|
const factory DateCellCalendarEvent.selectDay(DateTime day) = _SelectDay;
|
||||||
|
const factory DateCellCalendarEvent.selectDateRange(
|
||||||
|
DateTime? start,
|
||||||
|
DateTime? end,
|
||||||
|
) = _SelectDateRange;
|
||||||
const factory DateCellCalendarEvent.setTime(String time) = _Time;
|
const factory DateCellCalendarEvent.setTime(String time) = _Time;
|
||||||
|
const factory DateCellCalendarEvent.setEndTime(String endTime) = _EndTime;
|
||||||
const factory DateCellCalendarEvent.setIncludeTime(bool includeTime) =
|
const factory DateCellCalendarEvent.setIncludeTime(bool includeTime) =
|
||||||
_IncludeTime;
|
_IncludeTime;
|
||||||
|
const factory DateCellCalendarEvent.setIsRange(bool isRange) = _IsRange;
|
||||||
|
|
||||||
// date field type options are modified
|
// date field type options are modified
|
||||||
const factory DateCellCalendarEvent.setTimeFormat(TimeFormatPB timeFormat) =
|
const factory DateCellCalendarEvent.setTimeFormat(TimeFormatPB timeFormat) =
|
||||||
@ -262,12 +314,16 @@ class DateCellCalendarEvent with _$DateCellCalendarEvent {
|
|||||||
class DateCellCalendarState with _$DateCellCalendarState {
|
class DateCellCalendarState with _$DateCellCalendarState {
|
||||||
const factory DateCellCalendarState({
|
const factory DateCellCalendarState({
|
||||||
required DateTypeOptionPB dateTypeOptionPB,
|
required DateTypeOptionPB dateTypeOptionPB,
|
||||||
required CalendarFormat format,
|
required DateTime? startDay,
|
||||||
required DateTime focusedDay,
|
required DateTime? endDay,
|
||||||
required DateTime? dateTime,
|
required DateTime? dateTime,
|
||||||
|
required DateTime? endDateTime,
|
||||||
required String? time,
|
required String? time,
|
||||||
|
required String? endTime,
|
||||||
required bool includeTime,
|
required bool includeTime,
|
||||||
required String? timeFormatError,
|
required bool isRange,
|
||||||
|
required String? parseTimeError,
|
||||||
|
required String? parseEndTimeError,
|
||||||
required String timeHintText,
|
required String timeHintText,
|
||||||
}) = _DateCellCalendarState;
|
}) = _DateCellCalendarState;
|
||||||
|
|
||||||
@ -275,15 +331,20 @@ class DateCellCalendarState with _$DateCellCalendarState {
|
|||||||
DateTypeOptionPB dateTypeOptionPB,
|
DateTypeOptionPB dateTypeOptionPB,
|
||||||
DateCellDataPB? cellData,
|
DateCellDataPB? cellData,
|
||||||
) {
|
) {
|
||||||
final (dateTime, time, includeTime) = _dateDataFromCellData(cellData);
|
final (dateTime, endDateTime, time, endTime, includeTime, isRange) =
|
||||||
|
_dateDataFromCellData(cellData);
|
||||||
return DateCellCalendarState(
|
return DateCellCalendarState(
|
||||||
dateTypeOptionPB: dateTypeOptionPB,
|
dateTypeOptionPB: dateTypeOptionPB,
|
||||||
format: CalendarFormat.month,
|
startDay: isRange ? dateTime : null,
|
||||||
focusedDay: DateTime.now(),
|
endDay: isRange ? endDateTime : null,
|
||||||
dateTime: dateTime,
|
dateTime: dateTime,
|
||||||
|
endDateTime: endDateTime,
|
||||||
time: time,
|
time: time,
|
||||||
|
endTime: endTime,
|
||||||
includeTime: includeTime,
|
includeTime: includeTime,
|
||||||
timeFormatError: null,
|
isRange: isRange,
|
||||||
|
parseTimeError: null,
|
||||||
|
parseEndTimeError: null,
|
||||||
timeHintText: _timeHintText(dateTypeOptionPB),
|
timeHintText: _timeHintText(dateTypeOptionPB),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -300,21 +361,31 @@ String _timeHintText(DateTypeOptionPB typeOption) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
(DateTime?, String?, bool) _dateDataFromCellData(DateCellDataPB? cellData) {
|
(DateTime?, DateTime?, String?, String?, bool, bool) _dateDataFromCellData(
|
||||||
|
DateCellDataPB? cellData,
|
||||||
|
) {
|
||||||
// a null DateCellDataPB may be returned, indicating that all the fields are
|
// a null DateCellDataPB may be returned, indicating that all the fields are
|
||||||
// at their default values: empty strings and false booleans
|
// at their default values: empty strings and false booleans
|
||||||
if (cellData == null) {
|
if (cellData == null) {
|
||||||
return (null, null, false);
|
return (null, null, null, null, false, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
DateTime? dateTime;
|
DateTime? dateTime;
|
||||||
String? time;
|
String? time;
|
||||||
|
DateTime? endDateTime;
|
||||||
|
String? endTime;
|
||||||
if (cellData.hasTimestamp()) {
|
if (cellData.hasTimestamp()) {
|
||||||
final timestamp = cellData.timestamp * 1000;
|
final timestamp = cellData.timestamp * 1000;
|
||||||
dateTime = DateTime.fromMillisecondsSinceEpoch(timestamp.toInt());
|
dateTime = DateTime.fromMillisecondsSinceEpoch(timestamp.toInt());
|
||||||
time = cellData.time;
|
time = cellData.time;
|
||||||
|
if (cellData.hasEndTimestamp()) {
|
||||||
|
final endTimestamp = cellData.endTimestamp * 1000;
|
||||||
|
endDateTime = DateTime.fromMillisecondsSinceEpoch(endTimestamp.toInt());
|
||||||
|
endTime = cellData.endTime;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
final bool includeTime = cellData.includeTime;
|
final bool includeTime = cellData.includeTime;
|
||||||
|
final bool isRange = cellData.isRange;
|
||||||
|
|
||||||
return (dateTime, time, includeTime);
|
return (dateTime, endDateTime, time, endTime, includeTime, isRange);
|
||||||
}
|
}
|
||||||
|
@ -74,7 +74,7 @@ class _DateCellState extends GridCellState<GridDateCell> {
|
|||||||
controller: _popover,
|
controller: _popover,
|
||||||
triggerActions: PopoverTriggerFlags.none,
|
triggerActions: PopoverTriggerFlags.none,
|
||||||
direction: PopoverDirection.bottomWithLeftAligned,
|
direction: PopoverDirection.bottomWithLeftAligned,
|
||||||
constraints: BoxConstraints.loose(const Size(260, 520)),
|
constraints: BoxConstraints.loose(const Size(260, 620)),
|
||||||
margin: EdgeInsets.zero,
|
margin: EdgeInsets.zero,
|
||||||
child: GridDateCellText(
|
child: GridDateCellText(
|
||||||
dateStr: state.dateStr,
|
dateStr: state.dateStr,
|
||||||
|
@ -79,8 +79,19 @@ class DateCellState with _$DateCellState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
String _dateStrFromCellData(DateCellDataPB? cellData) {
|
String _dateStrFromCellData(DateCellDataPB? cellData) {
|
||||||
|
if (cellData == null || !cellData.hasTimestamp()) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
String dateStr = "";
|
String dateStr = "";
|
||||||
if (cellData != null) {
|
if (cellData.isRange) {
|
||||||
|
if (cellData.includeTime) {
|
||||||
|
dateStr =
|
||||||
|
"${cellData.date} ${cellData.time} → ${cellData.endDate} ${cellData.endTime}";
|
||||||
|
} else {
|
||||||
|
dateStr = "${cellData.date} → ${cellData.endDate}";
|
||||||
|
}
|
||||||
|
} else {
|
||||||
if (cellData.includeTime) {
|
if (cellData.includeTime) {
|
||||||
dateStr = "${cellData.date} ${cellData.time}";
|
dateStr = "${cellData.date} ${cellData.time}";
|
||||||
} else {
|
} else {
|
||||||
|
@ -3,6 +3,8 @@ import 'package:appflowy/generated/locale_keys.g.dart';
|
|||||||
import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart';
|
import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart';
|
||||||
import 'package:appflowy/plugins/database_view/application/field/type_option/type_option_context.dart';
|
import 'package:appflowy/plugins/database_view/application/field/type_option/type_option_context.dart';
|
||||||
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/type_option/timestamp.dart';
|
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/type_option/timestamp.dart';
|
||||||
|
import 'package:appflowy/workspace/presentation/widgets/toggle/toggle.dart';
|
||||||
|
import 'package:appflowy/workspace/presentation/widgets/toggle/toggle_style.dart';
|
||||||
import 'package:appflowy_backend/log.dart';
|
import 'package:appflowy_backend/log.dart';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-database2/date_entities.pb.dart';
|
import 'package:appflowy_backend/protobuf/flowy-database2/date_entities.pb.dart';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-error/errors.pbserver.dart';
|
import 'package:appflowy_backend/protobuf/flowy-error/errors.pbserver.dart';
|
||||||
@ -111,18 +113,31 @@ class _CellCalendarWidgetState extends State<_CellCalendarWidget> {
|
|||||||
duration: const Duration(milliseconds: 300),
|
duration: const Duration(milliseconds: 300),
|
||||||
child: state.includeTime
|
child: state.includeTime
|
||||||
? _TimeTextField(
|
? _TimeTextField(
|
||||||
|
isEndTime: false,
|
||||||
timeStr: state.time,
|
timeStr: state.time,
|
||||||
popoverMutex: popoverMutex,
|
popoverMutex: popoverMutex,
|
||||||
)
|
)
|
||||||
: const SizedBox.shrink(),
|
: const SizedBox.shrink(),
|
||||||
),
|
),
|
||||||
|
if (state.includeTime && state.isRange) const VSpace(8.0),
|
||||||
|
AnimatedSwitcher(
|
||||||
|
duration: const Duration(milliseconds: 300),
|
||||||
|
child: state.includeTime && state.isRange
|
||||||
|
? _TimeTextField(
|
||||||
|
isEndTime: true,
|
||||||
|
timeStr: state.endTime,
|
||||||
|
popoverMutex: popoverMutex,
|
||||||
|
)
|
||||||
|
: const SizedBox.shrink(),
|
||||||
|
),
|
||||||
const DatePicker(),
|
const DatePicker(),
|
||||||
const VSpace(8.0),
|
const TypeOptionSeparator(spacing: 12.0),
|
||||||
const TypeOptionSeparator(spacing: 8.0),
|
const EndTimeButton(),
|
||||||
|
const VSpace(4.0),
|
||||||
const _IncludeTimeButton(),
|
const _IncludeTimeButton(),
|
||||||
const TypeOptionSeparator(spacing: 8.0),
|
const TypeOptionSeparator(spacing: 8.0),
|
||||||
DateTypeOptionButton(popoverMutex: popoverMutex),
|
DateTypeOptionButton(popoverMutex: popoverMutex),
|
||||||
VSpace(GridSize.typeOptionSeparatorHeight),
|
const VSpace(4.0),
|
||||||
const ClearDateButton(),
|
const ClearDateButton(),
|
||||||
];
|
];
|
||||||
|
|
||||||
@ -145,9 +160,17 @@ class _CellCalendarWidgetState extends State<_CellCalendarWidget> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class DatePicker extends StatelessWidget {
|
class DatePicker extends StatefulWidget {
|
||||||
const DatePicker({super.key});
|
const DatePicker({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<DatePicker> createState() => _DatePickerState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _DatePickerState extends State<DatePicker> {
|
||||||
|
DateTime _focusedDay = DateTime.now();
|
||||||
|
CalendarFormat _calendarFormat = CalendarFormat.month;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return BlocBuilder<DateCellCalendarBloc, DateCellCalendarState>(
|
return BlocBuilder<DateCellCalendarBloc, DateCellCalendarState>(
|
||||||
@ -162,10 +185,15 @@ class DatePicker extends StatelessWidget {
|
|||||||
child: TableCalendar(
|
child: TableCalendar(
|
||||||
firstDay: kFirstDay,
|
firstDay: kFirstDay,
|
||||||
lastDay: kLastDay,
|
lastDay: kLastDay,
|
||||||
focusedDay: state.focusedDay,
|
focusedDay: _focusedDay,
|
||||||
rowHeight: 26.0 + 7.0,
|
rowHeight: 26.0 + 7.0,
|
||||||
calendarFormat: state.format,
|
calendarFormat: _calendarFormat,
|
||||||
daysOfWeekHeight: 17.0 + 8.0,
|
daysOfWeekHeight: 17.0 + 8.0,
|
||||||
|
rangeSelectionMode: state.isRange
|
||||||
|
? RangeSelectionMode.enforced
|
||||||
|
: RangeSelectionMode.disabled,
|
||||||
|
rangeStartDay: state.isRange ? state.startDay : null,
|
||||||
|
rangeEndDay: state.isRange ? state.endDay : null,
|
||||||
headerStyle: HeaderStyle(
|
headerStyle: HeaderStyle(
|
||||||
formatButtonVisible: false,
|
formatButtonVisible: false,
|
||||||
titleCentered: true,
|
titleCentered: true,
|
||||||
@ -198,15 +226,29 @@ class DatePicker extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
weekendDecoration: boxDecoration,
|
weekendDecoration: boxDecoration,
|
||||||
outsideDecoration: boxDecoration,
|
outsideDecoration: boxDecoration,
|
||||||
|
rangeStartDecoration: boxDecoration.copyWith(
|
||||||
|
color: Theme.of(context).colorScheme.primary,
|
||||||
|
),
|
||||||
|
rangeEndDecoration: boxDecoration.copyWith(
|
||||||
|
color: Theme.of(context).colorScheme.primary,
|
||||||
|
),
|
||||||
defaultTextStyle: textStyle,
|
defaultTextStyle: textStyle,
|
||||||
weekendTextStyle: textStyle,
|
weekendTextStyle: textStyle,
|
||||||
selectedTextStyle: textStyle.copyWith(
|
selectedTextStyle: textStyle.copyWith(
|
||||||
color: Theme.of(context).colorScheme.surface,
|
color: Theme.of(context).colorScheme.surface,
|
||||||
),
|
),
|
||||||
|
rangeStartTextStyle: textStyle.copyWith(
|
||||||
|
color: Theme.of(context).colorScheme.surface,
|
||||||
|
),
|
||||||
|
rangeEndTextStyle: textStyle.copyWith(
|
||||||
|
color: Theme.of(context).colorScheme.surface,
|
||||||
|
),
|
||||||
todayTextStyle: textStyle,
|
todayTextStyle: textStyle,
|
||||||
outsideTextStyle: textStyle.copyWith(
|
outsideTextStyle: textStyle.copyWith(
|
||||||
color: Theme.of(context).disabledColor,
|
color: Theme.of(context).disabledColor,
|
||||||
),
|
),
|
||||||
|
rangeHighlightColor:
|
||||||
|
Theme.of(context).colorScheme.secondaryContainer,
|
||||||
),
|
),
|
||||||
calendarBuilders: CalendarBuilders(
|
calendarBuilders: CalendarBuilders(
|
||||||
dowBuilder: (context, day) {
|
dowBuilder: (context, day) {
|
||||||
@ -223,22 +265,24 @@ class DatePicker extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
selectedDayPredicate: (day) => isSameDay(state.dateTime, day),
|
selectedDayPredicate: (day) =>
|
||||||
|
state.isRange ? false : isSameDay(state.dateTime, day),
|
||||||
onDaySelected: (selectedDay, focusedDay) {
|
onDaySelected: (selectedDay, focusedDay) {
|
||||||
context.read<DateCellCalendarBloc>().add(
|
context.read<DateCellCalendarBloc>().add(
|
||||||
DateCellCalendarEvent.selectDay(selectedDay.toLocal().date),
|
DateCellCalendarEvent.selectDay(selectedDay.toLocal().date),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
onFormatChanged: (format) {
|
onRangeSelected: (start, end, focusedDay) {
|
||||||
context
|
context.read<DateCellCalendarBloc>().add(
|
||||||
.read<DateCellCalendarBloc>()
|
DateCellCalendarEvent.selectDateRange(start, end),
|
||||||
.add(DateCellCalendarEvent.setCalFormat(format));
|
);
|
||||||
},
|
|
||||||
onPageChanged: (focusedDay) {
|
|
||||||
context
|
|
||||||
.read<DateCellCalendarBloc>()
|
|
||||||
.add(DateCellCalendarEvent.setFocusedDay(focusedDay));
|
|
||||||
},
|
},
|
||||||
|
onFormatChanged: (calendarFormat) => setState(() {
|
||||||
|
_calendarFormat = calendarFormat;
|
||||||
|
}),
|
||||||
|
onPageChanged: (focusedDay) => setState(() {
|
||||||
|
_focusedDay = focusedDay;
|
||||||
|
}),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@ -268,13 +312,57 @@ class _IncludeTimeButton extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@visibleForTesting
|
||||||
|
class EndTimeButton extends StatelessWidget {
|
||||||
|
const EndTimeButton({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return BlocSelector<DateCellCalendarBloc, DateCellCalendarState, bool>(
|
||||||
|
selector: (state) => state.isRange,
|
||||||
|
builder: (context, isRange) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 12.0),
|
||||||
|
child: SizedBox(
|
||||||
|
height: GridSize.popoverItemHeight,
|
||||||
|
child: Padding(
|
||||||
|
padding: GridSize.typeOptionContentInsets,
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
FlowySvg(
|
||||||
|
FlowySvgs.date_s,
|
||||||
|
color: Theme.of(context).iconTheme.color,
|
||||||
|
),
|
||||||
|
const HSpace(6),
|
||||||
|
FlowyText.medium(LocaleKeys.grid_field_isRange.tr()),
|
||||||
|
const Spacer(),
|
||||||
|
Toggle(
|
||||||
|
value: isRange,
|
||||||
|
onChanged: (value) => context
|
||||||
|
.read<DateCellCalendarBloc>()
|
||||||
|
.add(DateCellCalendarEvent.setIsRange(!value)),
|
||||||
|
style: ToggleStyle.big,
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class _TimeTextField extends StatefulWidget {
|
class _TimeTextField extends StatefulWidget {
|
||||||
|
final bool isEndTime;
|
||||||
final String? timeStr;
|
final String? timeStr;
|
||||||
final PopoverMutex popoverMutex;
|
final PopoverMutex popoverMutex;
|
||||||
|
|
||||||
const _TimeTextField({
|
const _TimeTextField({
|
||||||
required this.timeStr,
|
required this.timeStr,
|
||||||
required this.popoverMutex,
|
required this.popoverMutex,
|
||||||
|
required this.isEndTime,
|
||||||
Key? key,
|
Key? key,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
@ -309,21 +397,41 @@ class _TimeTextFieldState extends State<_TimeTextField> {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return BlocConsumer<DateCellCalendarBloc, DateCellCalendarState>(
|
return BlocConsumer<DateCellCalendarBloc, DateCellCalendarState>(
|
||||||
listener: (context, state) => _textController.text = state.time ?? "",
|
listener: (context, state) {
|
||||||
|
if (widget.isEndTime) {
|
||||||
|
_textController.text = state.endTime ?? "";
|
||||||
|
} else {
|
||||||
|
_textController.text = state.time ?? "";
|
||||||
|
}
|
||||||
|
},
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
|
String text = "";
|
||||||
|
if (!widget.isEndTime && state.time != null) {
|
||||||
|
text = state.time!;
|
||||||
|
} else if (state.endTime != null) {
|
||||||
|
text = state.endTime!;
|
||||||
|
}
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 18.0),
|
padding: const EdgeInsets.symmetric(horizontal: 18.0),
|
||||||
child: FlowyTextField(
|
child: FlowyTextField(
|
||||||
text: state.time ?? "",
|
text: text,
|
||||||
focusNode: _focusNode,
|
focusNode: _focusNode,
|
||||||
controller: _textController,
|
controller: _textController,
|
||||||
submitOnLeave: true,
|
submitOnLeave: true,
|
||||||
hintText: state.timeHintText,
|
hintText: state.timeHintText,
|
||||||
errorText: state.timeFormatError,
|
errorText: widget.isEndTime
|
||||||
|
? state.parseEndTimeError
|
||||||
|
: state.parseTimeError,
|
||||||
onSubmitted: (timeStr) {
|
onSubmitted: (timeStr) {
|
||||||
|
if (widget.isEndTime) {
|
||||||
|
context
|
||||||
|
.read<DateCellCalendarBloc>()
|
||||||
|
.add(DateCellCalendarEvent.setEndTime(timeStr));
|
||||||
|
} else {
|
||||||
context
|
context
|
||||||
.read<DateCellCalendarBloc>()
|
.read<DateCellCalendarBloc>()
|
||||||
.add(DateCellCalendarEvent.setTime(timeStr));
|
.add(DateCellCalendarEvent.setTime(timeStr));
|
||||||
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
@ -107,7 +107,8 @@ class FlowyTextFieldState extends State<FlowyTextField> {
|
|||||||
maxLengthEnforcement: MaxLengthEnforcement.truncateAfterCompositionEnds,
|
maxLengthEnforcement: MaxLengthEnforcement.truncateAfterCompositionEnds,
|
||||||
style: Theme.of(context).textTheme.bodySmall,
|
style: Theme.of(context).textTheme.bodySmall,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
constraints: const BoxConstraints(maxHeight: 32),
|
constraints: BoxConstraints(
|
||||||
|
maxHeight: widget.errorText?.isEmpty ?? true ? 32 : 58),
|
||||||
contentPadding: const EdgeInsets.symmetric(horizontal: 12),
|
contentPadding: const EdgeInsets.symmetric(horizontal: 12),
|
||||||
enabledBorder: OutlineInputBorder(
|
enabledBorder: OutlineInputBorder(
|
||||||
borderSide: BorderSide(
|
borderSide: BorderSide(
|
||||||
@ -119,6 +120,10 @@ class FlowyTextFieldState extends State<FlowyTextField> {
|
|||||||
isDense: false,
|
isDense: false,
|
||||||
hintText: widget.hintText,
|
hintText: widget.hintText,
|
||||||
errorText: widget.errorText,
|
errorText: widget.errorText,
|
||||||
|
errorStyle: Theme.of(context)
|
||||||
|
.textTheme
|
||||||
|
.bodySmall!
|
||||||
|
.copyWith(color: Theme.of(context).colorScheme.error),
|
||||||
hintStyle: Theme.of(context)
|
hintStyle: Theme.of(context)
|
||||||
.textTheme
|
.textTheme
|
||||||
.bodySmall!
|
.bodySmall!
|
||||||
|
@ -424,6 +424,7 @@
|
|||||||
"numberFormat": "Number format",
|
"numberFormat": "Number format",
|
||||||
"dateFormat": "Date format",
|
"dateFormat": "Date format",
|
||||||
"includeTime": "Include time",
|
"includeTime": "Include time",
|
||||||
|
"isRange": "End date",
|
||||||
"dateFormatFriendly": "Month Day, Year",
|
"dateFormatFriendly": "Month Day, Year",
|
||||||
"dateFormatISO": "Year-Month-Day",
|
"dateFormatISO": "Year-Month-Day",
|
||||||
"dateFormatLocal": "Month/Day/Year",
|
"dateFormatLocal": "Month/Day/Year",
|
||||||
|
@ -19,7 +19,19 @@ pub struct DateCellDataPB {
|
|||||||
pub timestamp: i64,
|
pub timestamp: i64,
|
||||||
|
|
||||||
#[pb(index = 4)]
|
#[pb(index = 4)]
|
||||||
|
pub end_date: String,
|
||||||
|
|
||||||
|
#[pb(index = 5)]
|
||||||
|
pub end_time: String,
|
||||||
|
|
||||||
|
#[pb(index = 6)]
|
||||||
|
pub end_timestamp: i64,
|
||||||
|
|
||||||
|
#[pb(index = 7)]
|
||||||
pub include_time: bool,
|
pub include_time: bool,
|
||||||
|
|
||||||
|
#[pb(index = 8)]
|
||||||
|
pub is_range: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Default, ProtoBuf)]
|
#[derive(Clone, Debug, Default, ProtoBuf)]
|
||||||
@ -34,9 +46,18 @@ pub struct DateChangesetPB {
|
|||||||
pub time: Option<String>,
|
pub time: Option<String>,
|
||||||
|
|
||||||
#[pb(index = 4, one_of)]
|
#[pb(index = 4, one_of)]
|
||||||
pub include_time: Option<bool>,
|
pub end_date: Option<i64>,
|
||||||
|
|
||||||
#[pb(index = 5, one_of)]
|
#[pb(index = 5, one_of)]
|
||||||
|
pub end_time: Option<String>,
|
||||||
|
|
||||||
|
#[pb(index = 6, one_of)]
|
||||||
|
pub include_time: Option<bool>,
|
||||||
|
|
||||||
|
#[pb(index = 7, one_of)]
|
||||||
|
pub is_range: Option<bool>,
|
||||||
|
|
||||||
|
#[pb(index = 8, one_of)]
|
||||||
pub clear_flag: Option<bool>,
|
pub clear_flag: Option<bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -641,7 +641,10 @@ pub(crate) async fn update_date_cell_handler(
|
|||||||
let cell_changeset = DateCellChangeset {
|
let cell_changeset = DateCellChangeset {
|
||||||
date: data.date,
|
date: data.date,
|
||||||
time: data.time,
|
time: data.time,
|
||||||
|
end_date: data.end_date,
|
||||||
|
end_time: data.end_time,
|
||||||
include_time: data.include_time,
|
include_time: data.include_time,
|
||||||
|
is_range: data.is_range,
|
||||||
clear_flag: data.clear_flag,
|
clear_flag: data.clear_flag,
|
||||||
};
|
};
|
||||||
let database_editor = manager.get_database_with_view_id(&cell_id.view_id).await?;
|
let database_editor = manager.get_database_with_view_id(&cell_id.view_id).await?;
|
||||||
|
@ -210,9 +210,8 @@ pub fn insert_checkbox_cell(is_check: bool, field: &Field) -> Cell {
|
|||||||
pub fn insert_date_cell(timestamp: i64, include_time: Option<bool>, field: &Field) -> Cell {
|
pub fn insert_date_cell(timestamp: i64, include_time: Option<bool>, field: &Field) -> Cell {
|
||||||
let cell_data = serde_json::to_string(&DateCellChangeset {
|
let cell_data = serde_json::to_string(&DateCellChangeset {
|
||||||
date: Some(timestamp),
|
date: Some(timestamp),
|
||||||
time: None,
|
|
||||||
include_time,
|
include_time,
|
||||||
clear_flag: None,
|
..Default::default()
|
||||||
})
|
})
|
||||||
.unwrap();
|
.unwrap();
|
||||||
apply_cell_changeset(cell_data, None, field, None).unwrap()
|
apply_cell_changeset(cell_data, None, field, None).unwrap()
|
||||||
|
@ -27,7 +27,7 @@ mod tests {
|
|||||||
date: Some(1647251762),
|
date: Some(1647251762),
|
||||||
time: None,
|
time: None,
|
||||||
include_time: None,
|
include_time: None,
|
||||||
clear_flag: None,
|
..Default::default()
|
||||||
},
|
},
|
||||||
None,
|
None,
|
||||||
"Mar 14, 2022",
|
"Mar 14, 2022",
|
||||||
@ -41,7 +41,7 @@ mod tests {
|
|||||||
date: Some(1647251762),
|
date: Some(1647251762),
|
||||||
time: None,
|
time: None,
|
||||||
include_time: None,
|
include_time: None,
|
||||||
clear_flag: None,
|
..Default::default()
|
||||||
},
|
},
|
||||||
None,
|
None,
|
||||||
"2022/03/14",
|
"2022/03/14",
|
||||||
@ -55,7 +55,7 @@ mod tests {
|
|||||||
date: Some(1647251762),
|
date: Some(1647251762),
|
||||||
time: None,
|
time: None,
|
||||||
include_time: None,
|
include_time: None,
|
||||||
clear_flag: None,
|
..Default::default()
|
||||||
},
|
},
|
||||||
None,
|
None,
|
||||||
"2022-03-14",
|
"2022-03-14",
|
||||||
@ -69,7 +69,7 @@ mod tests {
|
|||||||
date: Some(1647251762),
|
date: Some(1647251762),
|
||||||
time: None,
|
time: None,
|
||||||
include_time: None,
|
include_time: None,
|
||||||
clear_flag: None,
|
..Default::default()
|
||||||
},
|
},
|
||||||
None,
|
None,
|
||||||
"03/14/2022",
|
"03/14/2022",
|
||||||
@ -83,7 +83,7 @@ mod tests {
|
|||||||
date: Some(1647251762),
|
date: Some(1647251762),
|
||||||
time: None,
|
time: None,
|
||||||
include_time: None,
|
include_time: None,
|
||||||
clear_flag: None,
|
..Default::default()
|
||||||
},
|
},
|
||||||
None,
|
None,
|
||||||
"14/03/2022",
|
"14/03/2022",
|
||||||
@ -109,7 +109,7 @@ mod tests {
|
|||||||
date: Some(1653609600),
|
date: Some(1653609600),
|
||||||
time: None,
|
time: None,
|
||||||
include_time: Some(true),
|
include_time: Some(true),
|
||||||
clear_flag: None,
|
..Default::default()
|
||||||
},
|
},
|
||||||
None,
|
None,
|
||||||
"May 27, 2022 00:00",
|
"May 27, 2022 00:00",
|
||||||
@ -121,7 +121,7 @@ mod tests {
|
|||||||
date: Some(1653609600),
|
date: Some(1653609600),
|
||||||
time: Some("9:00".to_owned()),
|
time: Some("9:00".to_owned()),
|
||||||
include_time: Some(true),
|
include_time: Some(true),
|
||||||
clear_flag: None,
|
..Default::default()
|
||||||
},
|
},
|
||||||
None,
|
None,
|
||||||
"May 27, 2022 09:00",
|
"May 27, 2022 09:00",
|
||||||
@ -133,7 +133,7 @@ mod tests {
|
|||||||
date: Some(1653609600),
|
date: Some(1653609600),
|
||||||
time: Some("23:00".to_owned()),
|
time: Some("23:00".to_owned()),
|
||||||
include_time: Some(true),
|
include_time: Some(true),
|
||||||
clear_flag: None,
|
..Default::default()
|
||||||
},
|
},
|
||||||
None,
|
None,
|
||||||
"May 27, 2022 23:00",
|
"May 27, 2022 23:00",
|
||||||
@ -147,7 +147,7 @@ mod tests {
|
|||||||
date: Some(1653609600),
|
date: Some(1653609600),
|
||||||
time: None,
|
time: None,
|
||||||
include_time: Some(true),
|
include_time: Some(true),
|
||||||
clear_flag: None,
|
..Default::default()
|
||||||
},
|
},
|
||||||
None,
|
None,
|
||||||
"May 27, 2022 12:00 AM",
|
"May 27, 2022 12:00 AM",
|
||||||
@ -159,7 +159,7 @@ mod tests {
|
|||||||
date: Some(1653609600),
|
date: Some(1653609600),
|
||||||
time: Some("9:00 AM".to_owned()),
|
time: Some("9:00 AM".to_owned()),
|
||||||
include_time: Some(true),
|
include_time: Some(true),
|
||||||
clear_flag: None,
|
..Default::default()
|
||||||
},
|
},
|
||||||
None,
|
None,
|
||||||
"May 27, 2022 09:00 AM",
|
"May 27, 2022 09:00 AM",
|
||||||
@ -171,7 +171,7 @@ mod tests {
|
|||||||
date: Some(1653609600),
|
date: Some(1653609600),
|
||||||
time: Some("11:23 pm".to_owned()),
|
time: Some("11:23 pm".to_owned()),
|
||||||
include_time: Some(true),
|
include_time: Some(true),
|
||||||
clear_flag: None,
|
..Default::default()
|
||||||
},
|
},
|
||||||
None,
|
None,
|
||||||
"May 27, 2022 11:23 PM",
|
"May 27, 2022 11:23 PM",
|
||||||
@ -195,7 +195,7 @@ mod tests {
|
|||||||
date: Some(1653609600),
|
date: Some(1653609600),
|
||||||
time: Some("1:".to_owned()),
|
time: Some("1:".to_owned()),
|
||||||
include_time: Some(true),
|
include_time: Some(true),
|
||||||
clear_flag: None,
|
..Default::default()
|
||||||
},
|
},
|
||||||
None,
|
None,
|
||||||
"May 27, 2022 01:00",
|
"May 27, 2022 01:00",
|
||||||
@ -216,7 +216,7 @@ mod tests {
|
|||||||
date: Some(1653609600),
|
date: Some(1653609600),
|
||||||
time: Some("".to_owned()),
|
time: Some("".to_owned()),
|
||||||
include_time: Some(true),
|
include_time: Some(true),
|
||||||
clear_flag: None,
|
..Default::default()
|
||||||
},
|
},
|
||||||
None,
|
None,
|
||||||
"May 27, 2022 01:00",
|
"May 27, 2022 01:00",
|
||||||
@ -235,7 +235,7 @@ mod tests {
|
|||||||
date: Some(1653609600),
|
date: Some(1653609600),
|
||||||
time: Some("00:00".to_owned()),
|
time: Some("00:00".to_owned()),
|
||||||
include_time: Some(true),
|
include_time: Some(true),
|
||||||
clear_flag: None,
|
..Default::default()
|
||||||
},
|
},
|
||||||
None,
|
None,
|
||||||
"May 27, 2022 00:00",
|
"May 27, 2022 00:00",
|
||||||
@ -256,7 +256,7 @@ mod tests {
|
|||||||
date: Some(1653609600),
|
date: Some(1653609600),
|
||||||
time: Some("1:00 am".to_owned()),
|
time: Some("1:00 am".to_owned()),
|
||||||
include_time: Some(true),
|
include_time: Some(true),
|
||||||
clear_flag: None,
|
..Default::default()
|
||||||
},
|
},
|
||||||
None,
|
None,
|
||||||
"May 27, 2022 01:00 AM",
|
"May 27, 2022 01:00 AM",
|
||||||
@ -280,7 +280,7 @@ mod tests {
|
|||||||
date: Some(1653609600),
|
date: Some(1653609600),
|
||||||
time: Some("20:00".to_owned()),
|
time: Some("20:00".to_owned()),
|
||||||
include_time: Some(true),
|
include_time: Some(true),
|
||||||
clear_flag: None,
|
..Default::default()
|
||||||
},
|
},
|
||||||
None,
|
None,
|
||||||
"May 27, 2022 08:00 PM",
|
"May 27, 2022 08:00 PM",
|
||||||
@ -329,7 +329,7 @@ mod tests {
|
|||||||
date: Some(1700006400),
|
date: Some(1700006400),
|
||||||
time: Some("08:00".to_owned()),
|
time: Some("08:00".to_owned()),
|
||||||
include_time: Some(true),
|
include_time: Some(true),
|
||||||
clear_flag: None,
|
..Default::default()
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
assert_date(
|
assert_date(
|
||||||
@ -339,7 +339,7 @@ mod tests {
|
|||||||
date: Some(1701302400),
|
date: Some(1701302400),
|
||||||
time: None,
|
time: None,
|
||||||
include_time: None,
|
include_time: None,
|
||||||
clear_flag: None,
|
..Default::default()
|
||||||
},
|
},
|
||||||
Some(old_cell_data),
|
Some(old_cell_data),
|
||||||
"Nov 30, 2023 08:00",
|
"Nov 30, 2023 08:00",
|
||||||
@ -357,7 +357,7 @@ mod tests {
|
|||||||
date: Some(1700006400),
|
date: Some(1700006400),
|
||||||
time: Some("08:00".to_owned()),
|
time: Some("08:00".to_owned()),
|
||||||
include_time: Some(true),
|
include_time: Some(true),
|
||||||
clear_flag: None,
|
..Default::default()
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
assert_date(
|
assert_date(
|
||||||
@ -367,7 +367,7 @@ mod tests {
|
|||||||
date: None,
|
date: None,
|
||||||
time: Some("14:00".to_owned()),
|
time: Some("14:00".to_owned()),
|
||||||
include_time: None,
|
include_time: None,
|
||||||
clear_flag: None,
|
..Default::default()
|
||||||
},
|
},
|
||||||
Some(old_cell_data),
|
Some(old_cell_data),
|
||||||
"Nov 15, 2023 14:00",
|
"Nov 15, 2023 14:00",
|
||||||
@ -385,7 +385,7 @@ mod tests {
|
|||||||
date: Some(1700006400),
|
date: Some(1700006400),
|
||||||
time: Some("08:00".to_owned()),
|
time: Some("08:00".to_owned()),
|
||||||
include_time: Some(true),
|
include_time: Some(true),
|
||||||
clear_flag: None,
|
..Default::default()
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
assert_date(
|
assert_date(
|
||||||
@ -396,12 +396,142 @@ mod tests {
|
|||||||
time: None,
|
time: None,
|
||||||
include_time: Some(true),
|
include_time: Some(true),
|
||||||
clear_flag: Some(true),
|
clear_flag: Some(true),
|
||||||
|
..Default::default()
|
||||||
},
|
},
|
||||||
Some(old_cell_data),
|
Some(old_cell_data),
|
||||||
"",
|
"",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn end_date_time_test() {
|
||||||
|
let type_option = DateTypeOption::test();
|
||||||
|
let field = FieldBuilder::from_field_type(FieldType::DateTime).build();
|
||||||
|
|
||||||
|
assert_date(
|
||||||
|
&type_option,
|
||||||
|
&field,
|
||||||
|
DateCellChangeset {
|
||||||
|
date: Some(1653609600),
|
||||||
|
end_date: Some(1653782400),
|
||||||
|
include_time: Some(false),
|
||||||
|
is_range: Some(true),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
None,
|
||||||
|
"May 27, 2022 → May 29, 2022",
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_date(
|
||||||
|
&type_option,
|
||||||
|
&field,
|
||||||
|
DateCellChangeset {
|
||||||
|
date: Some(1653609600),
|
||||||
|
time: Some("20:00".to_owned()),
|
||||||
|
end_date: Some(1653782400),
|
||||||
|
end_time: Some("08:00".to_owned()),
|
||||||
|
include_time: Some(true),
|
||||||
|
is_range: Some(true),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
None,
|
||||||
|
"May 27, 2022 20:00 → May 29, 2022 08:00",
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_date(
|
||||||
|
&type_option,
|
||||||
|
&field,
|
||||||
|
DateCellChangeset {
|
||||||
|
date: Some(1653609600),
|
||||||
|
time: Some("20:00".to_owned()),
|
||||||
|
end_date: Some(1653782400),
|
||||||
|
include_time: Some(true),
|
||||||
|
is_range: Some(true),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
None,
|
||||||
|
"May 27, 2022 20:00 → May 29, 2022 00:00",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn turn_on_date_range() {
|
||||||
|
let type_option = DateTypeOption::test();
|
||||||
|
let field = FieldBuilder::from_field_type(FieldType::DateTime).build();
|
||||||
|
|
||||||
|
let old_cell_data = initialize_date_cell(
|
||||||
|
&type_option,
|
||||||
|
DateCellChangeset {
|
||||||
|
date: Some(1653609600),
|
||||||
|
time: Some("08:00".to_owned()),
|
||||||
|
include_time: Some(true),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
);
|
||||||
|
assert_date(
|
||||||
|
&type_option,
|
||||||
|
&field,
|
||||||
|
DateCellChangeset {
|
||||||
|
is_range: Some(true),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
Some(old_cell_data),
|
||||||
|
"May 27, 2022 08:00 → May 27, 2022 08:00",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn add_an_end_time() {
|
||||||
|
let type_option = DateTypeOption::test();
|
||||||
|
let field = FieldBuilder::from_field_type(FieldType::DateTime).build();
|
||||||
|
|
||||||
|
let old_cell_data = initialize_date_cell(
|
||||||
|
&type_option,
|
||||||
|
DateCellChangeset {
|
||||||
|
date: Some(1653609600),
|
||||||
|
time: Some("08:00".to_owned()),
|
||||||
|
include_time: Some(true),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
);
|
||||||
|
assert_date(
|
||||||
|
&type_option,
|
||||||
|
&field,
|
||||||
|
DateCellChangeset {
|
||||||
|
date: None,
|
||||||
|
time: None,
|
||||||
|
end_date: Some(1700006400),
|
||||||
|
end_time: Some("16:00".to_owned()),
|
||||||
|
include_time: Some(true),
|
||||||
|
is_range: Some(true),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
Some(old_cell_data),
|
||||||
|
"May 27, 2022 08:00 → Nov 15, 2023 16:00",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[should_panic]
|
||||||
|
fn end_date_with_no_start_date() {
|
||||||
|
let type_option = DateTypeOption::test();
|
||||||
|
let field = FieldBuilder::from_field_type(FieldType::DateTime).build();
|
||||||
|
|
||||||
|
assert_date(
|
||||||
|
&type_option,
|
||||||
|
&field,
|
||||||
|
DateCellChangeset {
|
||||||
|
date: None,
|
||||||
|
end_date: Some(1653782400),
|
||||||
|
include_time: Some(false),
|
||||||
|
is_range: Some(true),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
None,
|
||||||
|
"→ May 29, 2022",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
fn assert_date(
|
fn assert_date(
|
||||||
type_option: &DateTypeOption,
|
type_option: &DateTypeOption,
|
||||||
field: &Field,
|
field: &Field,
|
||||||
|
@ -70,15 +70,24 @@ impl TypeOptionCellDataSerde for DateTypeOption {
|
|||||||
&self,
|
&self,
|
||||||
cell_data: <Self as TypeOption>::CellData,
|
cell_data: <Self as TypeOption>::CellData,
|
||||||
) -> <Self as TypeOption>::CellProtobufType {
|
) -> <Self as TypeOption>::CellProtobufType {
|
||||||
let timestamp = cell_data.timestamp;
|
|
||||||
let include_time = cell_data.include_time;
|
let include_time = cell_data.include_time;
|
||||||
|
let is_range = cell_data.is_range;
|
||||||
|
|
||||||
|
let timestamp = cell_data.timestamp;
|
||||||
let (date, time) = self.formatted_date_time_from_timestamp(×tamp);
|
let (date, time) = self.formatted_date_time_from_timestamp(×tamp);
|
||||||
|
|
||||||
|
let end_timestamp = cell_data.end_timestamp;
|
||||||
|
let (end_date, end_time) = self.formatted_date_time_from_timestamp(&end_timestamp);
|
||||||
|
|
||||||
DateCellDataPB {
|
DateCellDataPB {
|
||||||
date,
|
date,
|
||||||
time,
|
time,
|
||||||
timestamp: timestamp.unwrap_or_default(),
|
timestamp: timestamp.unwrap_or_default(),
|
||||||
|
end_date,
|
||||||
|
end_time,
|
||||||
|
end_timestamp: end_timestamp.unwrap_or_default(),
|
||||||
include_time,
|
include_time,
|
||||||
|
is_range,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -135,6 +144,8 @@ impl DateTypeOption {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// combine the changeset_timestamp and parsed_time if provided. if
|
||||||
|
/// changeset_timestamp is None, fallback to previous_timestamp
|
||||||
fn timestamp_from_parsed_time_previous_and_new_timestamp(
|
fn timestamp_from_parsed_time_previous_and_new_timestamp(
|
||||||
&self,
|
&self,
|
||||||
parsed_time: Option<NaiveTime>,
|
parsed_time: Option<NaiveTime>,
|
||||||
@ -142,7 +153,7 @@ impl DateTypeOption {
|
|||||||
changeset_timestamp: Option<i64>,
|
changeset_timestamp: Option<i64>,
|
||||||
) -> Option<i64> {
|
) -> Option<i64> {
|
||||||
if let Some(time) = parsed_time {
|
if let Some(time) = parsed_time {
|
||||||
// a valid time is provided, so we replace the time component of old
|
// a valid time is provided, so we replace the time component of old timestamp
|
||||||
// (or new timestamp if provided) with it.
|
// (or new timestamp if provided) with it.
|
||||||
let utc_date = changeset_timestamp
|
let utc_date = changeset_timestamp
|
||||||
.or(previous_timestamp)
|
.or(previous_timestamp)
|
||||||
@ -206,13 +217,30 @@ impl CellDataDecoder for DateTypeOption {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn stringify_cell_data(&self, cell_data: <Self as TypeOption>::CellData) -> String {
|
fn stringify_cell_data(&self, cell_data: <Self as TypeOption>::CellData) -> String {
|
||||||
let timestamp = cell_data.timestamp;
|
|
||||||
let include_time = cell_data.include_time;
|
let include_time = cell_data.include_time;
|
||||||
let (date_string, time_string) = self.formatted_date_time_from_timestamp(×tamp);
|
let timestamp = cell_data.timestamp;
|
||||||
|
let is_range = cell_data.is_range;
|
||||||
|
|
||||||
|
let (date, time) = self.formatted_date_time_from_timestamp(×tamp);
|
||||||
|
|
||||||
|
if is_range {
|
||||||
|
let (end_date, end_time) = match cell_data.end_timestamp {
|
||||||
|
Some(timestamp) => self.formatted_date_time_from_timestamp(&Some(timestamp)),
|
||||||
|
None => (date.clone(), time.clone()),
|
||||||
|
};
|
||||||
if include_time && timestamp.is_some() {
|
if include_time && timestamp.is_some() {
|
||||||
format!("{} {}", date_string, time_string)
|
format!("{} {} → {} {}", date, time, end_date, end_time)
|
||||||
|
.trim()
|
||||||
|
.to_string()
|
||||||
|
} else if timestamp.is_some() {
|
||||||
|
format!("{} → {}", date, end_date).trim().to_string()
|
||||||
} else {
|
} else {
|
||||||
date_string
|
"".to_string()
|
||||||
|
}
|
||||||
|
} else if include_time {
|
||||||
|
format!("{} {}", date, time).trim().to_string()
|
||||||
|
} else {
|
||||||
|
date
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -229,25 +257,33 @@ impl CellDataChangeset for DateTypeOption {
|
|||||||
cell: Option<Cell>,
|
cell: Option<Cell>,
|
||||||
) -> FlowyResult<(Cell, <Self as TypeOption>::CellData)> {
|
) -> FlowyResult<(Cell, <Self as TypeOption>::CellData)> {
|
||||||
// old date cell data
|
// old date cell data
|
||||||
let (previous_timestamp, include_time) = match cell {
|
let (previous_timestamp, previous_end_timestamp, include_time, is_range) = match cell {
|
||||||
Some(cell) => {
|
Some(cell) => {
|
||||||
let cell_data = DateCellData::from(&cell);
|
let cell_data = DateCellData::from(&cell);
|
||||||
(cell_data.timestamp, cell_data.include_time)
|
(
|
||||||
|
cell_data.timestamp,
|
||||||
|
cell_data.end_timestamp,
|
||||||
|
cell_data.include_time,
|
||||||
|
cell_data.is_range,
|
||||||
|
)
|
||||||
},
|
},
|
||||||
None => (None, false),
|
None => (None, None, false, false),
|
||||||
};
|
};
|
||||||
|
|
||||||
if changeset.clear_flag == Some(true) {
|
if changeset.clear_flag == Some(true) {
|
||||||
let cell_data = DateCellData {
|
let cell_data = DateCellData {
|
||||||
timestamp: None,
|
timestamp: None,
|
||||||
|
end_timestamp: None,
|
||||||
include_time,
|
include_time,
|
||||||
|
is_range,
|
||||||
};
|
};
|
||||||
|
|
||||||
return Ok((Cell::from(&cell_data), cell_data));
|
return Ok((Cell::from(&cell_data), cell_data));
|
||||||
}
|
}
|
||||||
|
|
||||||
// update include_time if necessary
|
// update include_time and is_range if necessary
|
||||||
let include_time = changeset.include_time.unwrap_or(include_time);
|
let include_time = changeset.include_time.unwrap_or(include_time);
|
||||||
|
let is_range = changeset.is_range.unwrap_or(is_range);
|
||||||
|
|
||||||
// Calculate the timestamp in the time zone specified in type option. If
|
// Calculate the timestamp in the time zone specified in type option. If
|
||||||
// a new timestamp is included in the changeset without an accompanying
|
// a new timestamp is included in the changeset without an accompanying
|
||||||
@ -255,17 +291,38 @@ impl CellDataChangeset for DateTypeOption {
|
|||||||
// order to change the day without changing the time, the old time string
|
// order to change the day without changing the time, the old time string
|
||||||
// should be passed in as well.
|
// should be passed in as well.
|
||||||
|
|
||||||
let parsed_time = self.naive_time_from_time_string(include_time, changeset.time)?;
|
// parse the time string, which is in the local timezone
|
||||||
|
let parsed_start_time = self.naive_time_from_time_string(include_time, changeset.time)?;
|
||||||
|
|
||||||
let timestamp = self.timestamp_from_parsed_time_previous_and_new_timestamp(
|
let timestamp = self.timestamp_from_parsed_time_previous_and_new_timestamp(
|
||||||
parsed_time,
|
parsed_start_time,
|
||||||
previous_timestamp,
|
previous_timestamp,
|
||||||
changeset.date,
|
changeset.date,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let end_timestamp =
|
||||||
|
if is_range && changeset.end_date.is_none() && previous_end_timestamp.is_none() {
|
||||||
|
// just toggled is_range so no passed in or existing end time data
|
||||||
|
timestamp
|
||||||
|
} else if is_range {
|
||||||
|
// parse the changeset's end time data or fallback to previous version
|
||||||
|
let parsed_end_time = self.naive_time_from_time_string(include_time, changeset.end_time)?;
|
||||||
|
|
||||||
|
self.timestamp_from_parsed_time_previous_and_new_timestamp(
|
||||||
|
parsed_end_time,
|
||||||
|
previous_end_timestamp,
|
||||||
|
changeset.end_date,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
// clear the end time data
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
let cell_data = DateCellData {
|
let cell_data = DateCellData {
|
||||||
timestamp,
|
timestamp,
|
||||||
|
end_timestamp,
|
||||||
include_time,
|
include_time,
|
||||||
|
is_range,
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok((Cell::from(&cell_data), cell_data))
|
Ok((Cell::from(&cell_data), cell_data))
|
||||||
|
@ -20,7 +20,10 @@ use crate::services::field::{TypeOptionCellData, CELL_DATA};
|
|||||||
pub struct DateCellChangeset {
|
pub struct DateCellChangeset {
|
||||||
pub date: Option<i64>,
|
pub date: Option<i64>,
|
||||||
pub time: Option<String>,
|
pub time: Option<String>,
|
||||||
|
pub end_date: Option<i64>,
|
||||||
|
pub end_time: Option<String>,
|
||||||
pub include_time: Option<bool>,
|
pub include_time: Option<bool>,
|
||||||
|
pub is_range: Option<bool>,
|
||||||
pub clear_flag: Option<bool>,
|
pub clear_flag: Option<bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -42,15 +45,20 @@ impl ToCellChangeset for DateCellChangeset {
|
|||||||
#[derive(Default, Clone, Debug, Serialize)]
|
#[derive(Default, Clone, Debug, Serialize)]
|
||||||
pub struct DateCellData {
|
pub struct DateCellData {
|
||||||
pub timestamp: Option<i64>,
|
pub timestamp: Option<i64>,
|
||||||
|
pub end_timestamp: Option<i64>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub include_time: bool,
|
pub include_time: bool,
|
||||||
|
#[serde(default)]
|
||||||
|
pub is_range: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DateCellData {
|
impl DateCellData {
|
||||||
pub fn new(timestamp: i64, include_time: bool) -> Self {
|
pub fn new(timestamp: i64, include_time: bool, is_range: bool) -> Self {
|
||||||
Self {
|
Self {
|
||||||
timestamp: Some(timestamp),
|
timestamp: Some(timestamp),
|
||||||
|
end_timestamp: None,
|
||||||
include_time,
|
include_time,
|
||||||
|
is_range,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -66,10 +74,16 @@ impl From<&Cell> for DateCellData {
|
|||||||
let timestamp = cell
|
let timestamp = cell
|
||||||
.get_str_value(CELL_DATA)
|
.get_str_value(CELL_DATA)
|
||||||
.and_then(|data| data.parse::<i64>().ok());
|
.and_then(|data| data.parse::<i64>().ok());
|
||||||
|
let end_timestamp = cell
|
||||||
|
.get_str_value("end_timestamp")
|
||||||
|
.and_then(|data| data.parse::<i64>().ok());
|
||||||
let include_time = cell.get_bool_value("include_time").unwrap_or_default();
|
let include_time = cell.get_bool_value("include_time").unwrap_or_default();
|
||||||
|
let is_range = cell.get_bool_value("is_range").unwrap_or_default();
|
||||||
Self {
|
Self {
|
||||||
timestamp,
|
timestamp,
|
||||||
|
end_timestamp,
|
||||||
include_time,
|
include_time,
|
||||||
|
is_range,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -78,7 +92,9 @@ impl From<&DateCellDataPB> for DateCellData {
|
|||||||
fn from(data: &DateCellDataPB) -> Self {
|
fn from(data: &DateCellDataPB) -> Self {
|
||||||
Self {
|
Self {
|
||||||
timestamp: Some(data.timestamp),
|
timestamp: Some(data.timestamp),
|
||||||
|
end_timestamp: Some(data.end_timestamp),
|
||||||
include_time: data.include_time,
|
include_time: data.include_time,
|
||||||
|
is_range: data.is_range,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -89,9 +105,17 @@ impl From<&DateCellData> for Cell {
|
|||||||
Some(timestamp) => timestamp.to_string(),
|
Some(timestamp) => timestamp.to_string(),
|
||||||
None => "".to_owned(),
|
None => "".to_owned(),
|
||||||
};
|
};
|
||||||
|
let end_timestamp_string = match cell_data.end_timestamp {
|
||||||
|
Some(timestamp) => timestamp.to_string(),
|
||||||
|
None => "".to_owned(),
|
||||||
|
};
|
||||||
|
// Most of the case, don't use these keys in other places. Otherwise, we should define
|
||||||
|
// constants for them.
|
||||||
new_cell_builder(FieldType::DateTime)
|
new_cell_builder(FieldType::DateTime)
|
||||||
.insert_str_value(CELL_DATA, timestamp_string)
|
.insert_str_value(CELL_DATA, timestamp_string)
|
||||||
|
.insert_str_value("end_timestamp", end_timestamp_string)
|
||||||
.insert_bool_value("include_time", cell_data.include_time)
|
.insert_bool_value("include_time", cell_data.include_time)
|
||||||
|
.insert_bool_value("is_range", cell_data.is_range)
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -118,7 +142,9 @@ impl<'de> serde::Deserialize<'de> for DateCellData {
|
|||||||
{
|
{
|
||||||
Ok(DateCellData {
|
Ok(DateCellData {
|
||||||
timestamp: Some(value),
|
timestamp: Some(value),
|
||||||
|
end_timestamp: None,
|
||||||
include_time: false,
|
include_time: false,
|
||||||
|
is_range: false,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -134,25 +160,36 @@ impl<'de> serde::Deserialize<'de> for DateCellData {
|
|||||||
M: serde::de::MapAccess<'de>,
|
M: serde::de::MapAccess<'de>,
|
||||||
{
|
{
|
||||||
let mut timestamp: Option<i64> = None;
|
let mut timestamp: Option<i64> = None;
|
||||||
|
let mut end_timestamp: Option<i64> = None;
|
||||||
let mut include_time: Option<bool> = None;
|
let mut include_time: Option<bool> = None;
|
||||||
|
let mut is_range: Option<bool> = None;
|
||||||
|
|
||||||
while let Some(key) = map.next_key()? {
|
while let Some(key) = map.next_key()? {
|
||||||
match key {
|
match key {
|
||||||
"timestamp" => {
|
"timestamp" => {
|
||||||
timestamp = map.next_value()?;
|
timestamp = map.next_value()?;
|
||||||
},
|
},
|
||||||
|
"end_timestamp" => {
|
||||||
|
end_timestamp = map.next_value()?;
|
||||||
|
},
|
||||||
"include_time" => {
|
"include_time" => {
|
||||||
include_time = map.next_value()?;
|
include_time = map.next_value()?;
|
||||||
},
|
},
|
||||||
|
"is_range" => {
|
||||||
|
is_range = map.next_value()?;
|
||||||
|
},
|
||||||
_ => {},
|
_ => {},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let include_time = include_time.unwrap_or_default();
|
let include_time = include_time.unwrap_or_default();
|
||||||
|
let is_range = is_range.unwrap_or_default();
|
||||||
|
|
||||||
Ok(DateCellData {
|
Ok(DateCellData {
|
||||||
timestamp,
|
timestamp,
|
||||||
|
end_timestamp,
|
||||||
include_time,
|
include_time,
|
||||||
|
is_range,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -26,13 +26,39 @@ mod tests {
|
|||||||
|
|
||||||
let data = DateCellData {
|
let data = DateCellData {
|
||||||
timestamp: Some(1647251762),
|
timestamp: Some(1647251762),
|
||||||
|
end_timestamp: None,
|
||||||
include_time: true,
|
include_time: true,
|
||||||
|
is_range: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
stringify_cell_data(&(&data).into(), &FieldType::RichText, &field_type, &field),
|
stringify_cell_data(&(&data).into(), &FieldType::RichText, &field_type, &field),
|
||||||
"Mar 14, 2022 09:56"
|
"Mar 14, 2022 09:56"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let data = DateCellData {
|
||||||
|
timestamp: Some(1647251762),
|
||||||
|
end_timestamp: Some(1648533809),
|
||||||
|
include_time: true,
|
||||||
|
is_range: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
stringify_cell_data(&(&data).into(), &FieldType::RichText, &field_type, &field),
|
||||||
|
"Mar 14, 2022 09:56"
|
||||||
|
);
|
||||||
|
|
||||||
|
let data = DateCellData {
|
||||||
|
timestamp: Some(1647251762),
|
||||||
|
end_timestamp: Some(1648533809),
|
||||||
|
include_time: true,
|
||||||
|
is_range: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
stringify_cell_data(&(&data).into(), &FieldType::RichText, &field_type, &field),
|
||||||
|
"Mar 14, 2022 09:56 → Mar 29, 2022 06:03"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn to_text_cell(s: String) -> Cell {
|
fn to_text_cell(s: String) -> Cell {
|
||||||
|
@ -476,6 +476,7 @@ mod tests {
|
|||||||
let mar_14_2022_cd = DateCellData {
|
let mar_14_2022_cd = DateCellData {
|
||||||
timestamp: Some(mar_14_2022.timestamp()),
|
timestamp: Some(mar_14_2022.timestamp()),
|
||||||
include_time: false,
|
include_time: false,
|
||||||
|
..Default::default()
|
||||||
};
|
};
|
||||||
let today = offset::Local::now();
|
let today = offset::Local::now();
|
||||||
let three_days_before = today.checked_add_signed(Duration::days(-3)).unwrap();
|
let three_days_before = today.checked_add_signed(Duration::days(-3)).unwrap();
|
||||||
@ -497,6 +498,7 @@ mod tests {
|
|||||||
cell_data: DateCellData {
|
cell_data: DateCellData {
|
||||||
timestamp: Some(today.timestamp()),
|
timestamp: Some(today.timestamp()),
|
||||||
include_time: false,
|
include_time: false,
|
||||||
|
..Default::default()
|
||||||
},
|
},
|
||||||
type_option: &local_date_type_option,
|
type_option: &local_date_type_option,
|
||||||
setting_content: r#"{"condition": 0, "hide_empty": false}"#.to_string(),
|
setting_content: r#"{"condition": 0, "hide_empty": false}"#.to_string(),
|
||||||
@ -507,6 +509,7 @@ mod tests {
|
|||||||
cell_data: DateCellData {
|
cell_data: DateCellData {
|
||||||
timestamp: Some(three_days_before.timestamp()),
|
timestamp: Some(three_days_before.timestamp()),
|
||||||
include_time: false,
|
include_time: false,
|
||||||
|
..Default::default()
|
||||||
},
|
},
|
||||||
type_option: &local_date_type_option,
|
type_option: &local_date_type_option,
|
||||||
setting_content: r#"{"condition": 0, "hide_empty": false}"#.to_string(),
|
setting_content: r#"{"condition": 0, "hide_empty": false}"#.to_string(),
|
||||||
@ -533,6 +536,7 @@ mod tests {
|
|||||||
.timestamp(),
|
.timestamp(),
|
||||||
),
|
),
|
||||||
include_time: false,
|
include_time: false,
|
||||||
|
..Default::default()
|
||||||
},
|
},
|
||||||
type_option: &local_date_type_option,
|
type_option: &local_date_type_option,
|
||||||
setting_content: r#"{"condition": 2, "hide_empty": false}"#.to_string(),
|
setting_content: r#"{"condition": 2, "hide_empty": false}"#.to_string(),
|
||||||
@ -557,6 +561,7 @@ mod tests {
|
|||||||
cell_data: DateCellData {
|
cell_data: DateCellData {
|
||||||
timestamp: Some(1685715999),
|
timestamp: Some(1685715999),
|
||||||
include_time: false,
|
include_time: false,
|
||||||
|
..Default::default()
|
||||||
},
|
},
|
||||||
type_option: &default_date_type_option,
|
type_option: &default_date_type_option,
|
||||||
setting_content: r#"{"condition": 1, "hide_empty": false}"#.to_string(),
|
setting_content: r#"{"condition": 1, "hide_empty": false}"#.to_string(),
|
||||||
@ -567,6 +572,7 @@ mod tests {
|
|||||||
cell_data: DateCellData {
|
cell_data: DateCellData {
|
||||||
timestamp: Some(1685802386),
|
timestamp: Some(1685802386),
|
||||||
include_time: false,
|
include_time: false,
|
||||||
|
..Default::default()
|
||||||
},
|
},
|
||||||
type_option: &default_date_type_option,
|
type_option: &default_date_type_option,
|
||||||
setting_content: r#"{"condition": 1, "hide_empty": false}"#.to_string(),
|
setting_content: r#"{"condition": 1, "hide_empty": false}"#.to_string(),
|
||||||
|
@ -331,7 +331,7 @@ impl<'a> TestRowBuilder<'a> {
|
|||||||
date: Some(data),
|
date: Some(data),
|
||||||
time,
|
time,
|
||||||
include_time,
|
include_time,
|
||||||
clear_flag: None,
|
..Default::default()
|
||||||
})
|
})
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let date_field = self.field_with_type(field_type);
|
let date_field = self.field_with_type(field_type);
|
||||||
|
@ -102,7 +102,10 @@ pub fn make_date_cell_string(timestamp: i64) -> String {
|
|||||||
serde_json::to_string(&DateCellChangeset {
|
serde_json::to_string(&DateCellChangeset {
|
||||||
date: Some(timestamp),
|
date: Some(timestamp),
|
||||||
time: None,
|
time: None,
|
||||||
|
end_date: None,
|
||||||
|
end_time: None,
|
||||||
include_time: Some(false),
|
include_time: Some(false),
|
||||||
|
is_range: Some(false),
|
||||||
clear_flag: None,
|
clear_flag: None,
|
||||||
})
|
})
|
||||||
.unwrap()
|
.unwrap()
|
||||||
|
@ -528,9 +528,7 @@ async fn update_date_cell_event_test() {
|
|||||||
.update_date_cell(DateChangesetPB {
|
.update_date_cell(DateChangesetPB {
|
||||||
cell_id: cell_path,
|
cell_id: cell_path,
|
||||||
date: Some(timestamp),
|
date: Some(timestamp),
|
||||||
time: None,
|
..Default::default()
|
||||||
include_time: None,
|
|
||||||
clear_flag: None,
|
|
||||||
})
|
})
|
||||||
.await;
|
.await;
|
||||||
assert!(error.is_none());
|
assert!(error.is_none());
|
||||||
@ -892,9 +890,7 @@ async fn create_calendar_event_test() {
|
|||||||
row_id: row.id,
|
row_id: row.id,
|
||||||
},
|
},
|
||||||
date: Some(timestamp()),
|
date: Some(timestamp()),
|
||||||
time: None,
|
..Default::default()
|
||||||
include_time: None,
|
|
||||||
clear_flag: None,
|
|
||||||
})
|
})
|
||||||
.await;
|
.await;
|
||||||
assert!(error.is_none());
|
assert!(error.is_none());
|
||||||
|
Loading…
x
Reference in New Issue
Block a user