mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
refactor: different TypeOption between DateTime and LastModified/CreatedAt (#3356)
* fix: stringify date cell includes time if true * refactor: LastModified and CreatedAt type option * chore: frontend implementation * chore: some adjustments and fix tests * fix: integration tests * chore: timestamp type option ui
This commit is contained in:
@ -218,7 +218,6 @@ void main() {
|
|||||||
|
|
||||||
await tester.assertDateCellInGrid(
|
await tester.assertDateCellInGrid(
|
||||||
rowIndex: 0,
|
rowIndex: 0,
|
||||||
fieldType: fieldType,
|
|
||||||
content: DateFormat('MMM dd, y').format(today),
|
content: DateFormat('MMM dd, y').format(today),
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -233,7 +232,6 @@ void main() {
|
|||||||
|
|
||||||
await tester.assertDateCellInGrid(
|
await tester.assertDateCellInGrid(
|
||||||
rowIndex: 0,
|
rowIndex: 0,
|
||||||
fieldType: fieldType,
|
|
||||||
content: DateFormat('MMM dd, y HH:mm').format(now),
|
content: DateFormat('MMM dd, y HH:mm').format(now),
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -247,7 +245,6 @@ void main() {
|
|||||||
|
|
||||||
await tester.assertDateCellInGrid(
|
await tester.assertDateCellInGrid(
|
||||||
rowIndex: 0,
|
rowIndex: 0,
|
||||||
fieldType: fieldType,
|
|
||||||
content: DateFormat('dd/MM/y HH:mm').format(now),
|
content: DateFormat('dd/MM/y HH:mm').format(now),
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -261,7 +258,6 @@ void main() {
|
|||||||
|
|
||||||
await tester.assertDateCellInGrid(
|
await tester.assertDateCellInGrid(
|
||||||
rowIndex: 0,
|
rowIndex: 0,
|
||||||
fieldType: fieldType,
|
|
||||||
content: DateFormat('dd/MM/y hh:mm a').format(now),
|
content: DateFormat('dd/MM/y hh:mm a').format(now),
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -273,7 +269,6 @@ void main() {
|
|||||||
|
|
||||||
await tester.assertDateCellInGrid(
|
await tester.assertDateCellInGrid(
|
||||||
rowIndex: 0,
|
rowIndex: 0,
|
||||||
fieldType: fieldType,
|
|
||||||
content: '',
|
content: '',
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -157,7 +157,6 @@ void main() {
|
|||||||
for (final (index, content) in dateCells.indexed) {
|
for (final (index, content) in dateCells.indexed) {
|
||||||
await tester.assertDateCellInGrid(
|
await tester.assertDateCellInGrid(
|
||||||
rowIndex: index,
|
rowIndex: index,
|
||||||
fieldType: FieldType.DateTime,
|
|
||||||
content: content,
|
content: content,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -43,6 +43,7 @@ import 'package:appflowy/plugins/database_view/widgets/row/cells/date_cell/date_
|
|||||||
import 'package:appflowy/plugins/database_view/widgets/row/cells/select_option_cell/extension.dart';
|
import 'package:appflowy/plugins/database_view/widgets/row/cells/select_option_cell/extension.dart';
|
||||||
import 'package:appflowy/plugins/database_view/widgets/row/cells/select_option_cell/select_option_editor.dart';
|
import 'package:appflowy/plugins/database_view/widgets/row/cells/select_option_cell/select_option_editor.dart';
|
||||||
import 'package:appflowy/plugins/database_view/widgets/row/cells/select_option_cell/text_field.dart';
|
import 'package:appflowy/plugins/database_view/widgets/row/cells/select_option_cell/text_field.dart';
|
||||||
|
import 'package:appflowy/plugins/database_view/widgets/row/cells/timestamp_cell/timestamp_cell.dart';
|
||||||
import 'package:appflowy/plugins/database_view/widgets/row/row_action.dart';
|
import 'package:appflowy/plugins/database_view/widgets/row/row_action.dart';
|
||||||
import 'package:appflowy/plugins/database_view/widgets/row/row_banner.dart';
|
import 'package:appflowy/plugins/database_view/widgets/row/row_banner.dart';
|
||||||
import 'package:appflowy/plugins/database_view/widgets/row/row_detail.dart';
|
import 'package:appflowy/plugins/database_view/widgets/row/row_detail.dart';
|
||||||
@ -262,15 +263,12 @@ extension AppFlowyDatabaseTest on WidgetTester {
|
|||||||
|
|
||||||
Future<void> assertDateCellInGrid({
|
Future<void> assertDateCellInGrid({
|
||||||
required int rowIndex,
|
required int rowIndex,
|
||||||
required FieldType fieldType,
|
|
||||||
required String content,
|
required String content,
|
||||||
}) async {
|
}) async {
|
||||||
final findRow = find.byType(GridRow, skipOffstage: false);
|
final findRow = find.byType(GridRow, skipOffstage: false);
|
||||||
final findCell = find.descendant(
|
final findCell = find.descendant(
|
||||||
of: findRow.at(rowIndex),
|
of: findRow.at(rowIndex),
|
||||||
matching: find.byWidgetPredicate(
|
matching: find.byType(GridDateCell),
|
||||||
(widget) => widget is GridDateCell && widget.fieldType == fieldType,
|
|
||||||
),
|
|
||||||
skipOffstage: false,
|
skipOffstage: false,
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -1287,7 +1285,7 @@ Finder finderForFieldType(FieldType fieldType) {
|
|||||||
return find.byType(GridDateCell, skipOffstage: false);
|
return find.byType(GridDateCell, skipOffstage: false);
|
||||||
case FieldType.LastEditedTime:
|
case FieldType.LastEditedTime:
|
||||||
case FieldType.CreatedTime:
|
case FieldType.CreatedTime:
|
||||||
return find.byType(GridDateCell, skipOffstage: false);
|
return find.byType(GridTimestampCell, skipOffstage: false);
|
||||||
case FieldType.SingleSelect:
|
case FieldType.SingleSelect:
|
||||||
return find.byType(GridSingleSelectCell, skipOffstage: false);
|
return find.byType(GridSingleSelectCell, skipOffstage: false);
|
||||||
case FieldType.MultiSelect:
|
case FieldType.MultiSelect:
|
||||||
|
@ -2,6 +2,7 @@ import 'package:appflowy_backend/protobuf/flowy-database2/checklist_entities.pb.
|
|||||||
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-database2/field_entities.pbenum.dart';
|
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-database2/select_option.pb.dart';
|
import 'package:appflowy_backend/protobuf/flowy-database2/select_option.pb.dart';
|
||||||
|
import 'package:appflowy_backend/protobuf/flowy-database2/timestamp_entities.pb.dart';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-database2/url_entities.pb.dart';
|
import 'package:appflowy_backend/protobuf/flowy-database2/url_entities.pb.dart';
|
||||||
|
|
||||||
import 'cell_controller.dart';
|
import 'cell_controller.dart';
|
||||||
@ -14,6 +15,7 @@ typedef SelectOptionCellController
|
|||||||
= CellController<SelectOptionCellDataPB, String>;
|
= CellController<SelectOptionCellDataPB, String>;
|
||||||
typedef ChecklistCellController = CellController<ChecklistCellDataPB, String>;
|
typedef ChecklistCellController = CellController<ChecklistCellDataPB, String>;
|
||||||
typedef DateCellController = CellController<DateCellDataPB, String>;
|
typedef DateCellController = CellController<DateCellDataPB, String>;
|
||||||
|
typedef TimestampCellController = CellController<TimestampCellDataPB, String>;
|
||||||
typedef URLCellController = CellController<URLCellDataPB, String>;
|
typedef URLCellController = CellController<URLCellDataPB, String>;
|
||||||
|
|
||||||
class CellControllerBuilder {
|
class CellControllerBuilder {
|
||||||
@ -41,14 +43,11 @@ class CellControllerBuilder {
|
|||||||
TextCellDataPersistence(cellContext: _cellContext),
|
TextCellDataPersistence(cellContext: _cellContext),
|
||||||
);
|
);
|
||||||
case FieldType.DateTime:
|
case FieldType.DateTime:
|
||||||
case FieldType.LastEditedTime:
|
|
||||||
case FieldType.CreatedTime:
|
|
||||||
final cellDataLoader = CellDataLoader(
|
final cellDataLoader = CellDataLoader(
|
||||||
cellContext: _cellContext,
|
cellContext: _cellContext,
|
||||||
parser: DateCellDataParser(),
|
parser: DateCellDataParser(),
|
||||||
reloadOnFieldChanged: true,
|
reloadOnFieldChanged: true,
|
||||||
);
|
);
|
||||||
|
|
||||||
return DateCellController(
|
return DateCellController(
|
||||||
cellContext: _cellContext,
|
cellContext: _cellContext,
|
||||||
cellCache: _cellCache,
|
cellCache: _cellCache,
|
||||||
@ -56,6 +55,20 @@ class CellControllerBuilder {
|
|||||||
cellDataPersistence:
|
cellDataPersistence:
|
||||||
TextCellDataPersistence(cellContext: _cellContext),
|
TextCellDataPersistence(cellContext: _cellContext),
|
||||||
);
|
);
|
||||||
|
case FieldType.LastEditedTime:
|
||||||
|
case FieldType.CreatedTime:
|
||||||
|
final cellDataLoader = CellDataLoader(
|
||||||
|
cellContext: _cellContext,
|
||||||
|
parser: TimestampCellDataParser(),
|
||||||
|
reloadOnFieldChanged: true,
|
||||||
|
);
|
||||||
|
return TimestampCellController(
|
||||||
|
cellContext: _cellContext,
|
||||||
|
cellCache: _cellCache,
|
||||||
|
cellDataLoader: cellDataLoader,
|
||||||
|
cellDataPersistence:
|
||||||
|
TextCellDataPersistence(cellContext: _cellContext),
|
||||||
|
);
|
||||||
case FieldType.Number:
|
case FieldType.Number:
|
||||||
final cellDataLoader = CellDataLoader(
|
final cellDataLoader = CellDataLoader(
|
||||||
cellContext: _cellContext,
|
cellContext: _cellContext,
|
||||||
|
@ -73,6 +73,16 @@ class DateCellDataParser implements CellDataParser<DateCellDataPB> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class TimestampCellDataParser implements CellDataParser<TimestampCellDataPB> {
|
||||||
|
@override
|
||||||
|
TimestampCellDataPB? parserData(List<int> data) {
|
||||||
|
if (data.isEmpty) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return TimestampCellDataPB.fromBuffer(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class SelectOptionCellDataParser
|
class SelectOptionCellDataParser
|
||||||
implements CellDataParser<SelectOptionCellDataPB> {
|
implements CellDataParser<SelectOptionCellDataPB> {
|
||||||
@override
|
@override
|
||||||
|
@ -4,6 +4,7 @@ import 'package:appflowy_backend/protobuf/flowy-database2/checklist_entities.pb.
|
|||||||
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-database2/row_entities.pb.dart';
|
import 'package:appflowy_backend/protobuf/flowy-database2/row_entities.pb.dart';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-database2/select_option.pb.dart';
|
import 'package:appflowy_backend/protobuf/flowy-database2/select_option.pb.dart';
|
||||||
|
import 'package:appflowy_backend/protobuf/flowy-database2/timestamp_entities.pb.dart';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-database2/url_entities.pb.dart';
|
import 'package:appflowy_backend/protobuf/flowy-database2/url_entities.pb.dart';
|
||||||
import 'package:dartz/dartz.dart';
|
import 'package:dartz/dartz.dart';
|
||||||
import 'package:appflowy_backend/dispatch/dispatch.dart';
|
import 'package:appflowy_backend/dispatch/dispatch.dart';
|
||||||
|
@ -27,13 +27,6 @@ class DateTypeOptionBloc
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
includeTime: (_IncludeTime value) {
|
|
||||||
emit(
|
|
||||||
state.copyWith(
|
|
||||||
typeOption: _updateTypeOption(includeTime: value.includeTime),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@ -42,7 +35,6 @@ class DateTypeOptionBloc
|
|||||||
DateTypeOptionPB _updateTypeOption({
|
DateTypeOptionPB _updateTypeOption({
|
||||||
DateFormatPB? dateFormat,
|
DateFormatPB? dateFormat,
|
||||||
TimeFormatPB? timeFormat,
|
TimeFormatPB? timeFormat,
|
||||||
bool? includeTime,
|
|
||||||
}) {
|
}) {
|
||||||
state.typeOption.freeze();
|
state.typeOption.freeze();
|
||||||
return state.typeOption.rebuild((typeOption) {
|
return state.typeOption.rebuild((typeOption) {
|
||||||
@ -63,8 +55,6 @@ class DateTypeOptionEvent with _$DateTypeOptionEvent {
|
|||||||
_DidSelectDateFormat;
|
_DidSelectDateFormat;
|
||||||
const factory DateTypeOptionEvent.didSelectTimeFormat(TimeFormatPB format) =
|
const factory DateTypeOptionEvent.didSelectTimeFormat(TimeFormatPB format) =
|
||||||
_DidSelectTimeFormat;
|
_DidSelectTimeFormat;
|
||||||
const factory DateTypeOptionEvent.includeTime(bool includeTime) =
|
|
||||||
_IncludeTime;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@freezed
|
@freezed
|
||||||
|
@ -0,0 +1,76 @@
|
|||||||
|
import 'package:appflowy_backend/protobuf/flowy-database2/date_entities.pb.dart';
|
||||||
|
import 'package:appflowy_backend/protobuf/flowy-database2/timestamp_entities.pb.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
|
import 'package:protobuf/protobuf.dart';
|
||||||
|
|
||||||
|
import 'type_option_context.dart';
|
||||||
|
part 'timestamp_bloc.freezed.dart';
|
||||||
|
|
||||||
|
class TimestampTypeOptionBloc
|
||||||
|
extends Bloc<TimestampTypeOptionEvent, TimestampTypeOptionState> {
|
||||||
|
TimestampTypeOptionBloc({
|
||||||
|
required TimestampTypeOptionContext typeOptionContext,
|
||||||
|
}) : super(TimestampTypeOptionState.initial(typeOptionContext.typeOption)) {
|
||||||
|
on<TimestampTypeOptionEvent>(
|
||||||
|
(event, emit) async {
|
||||||
|
event.map(
|
||||||
|
didSelectDateFormat: (_DidSelectDateFormat value) {
|
||||||
|
_updateTypeOption(dateFormat: value.format, emit: emit);
|
||||||
|
},
|
||||||
|
didSelectTimeFormat: (_DidSelectTimeFormat value) {
|
||||||
|
_updateTypeOption(timeFormat: value.format, emit: emit);
|
||||||
|
},
|
||||||
|
includeTime: (_IncludeTime value) {
|
||||||
|
_updateTypeOption(includeTime: value.includeTime, emit: emit);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _updateTypeOption({
|
||||||
|
DateFormatPB? dateFormat,
|
||||||
|
TimeFormatPB? timeFormat,
|
||||||
|
bool? includeTime,
|
||||||
|
required Emitter<TimestampTypeOptionState> emit,
|
||||||
|
}) {
|
||||||
|
state.typeOption.freeze();
|
||||||
|
final newTypeOption = state.typeOption.rebuild((typeOption) {
|
||||||
|
if (dateFormat != null) {
|
||||||
|
typeOption.dateFormat = dateFormat;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (timeFormat != null) {
|
||||||
|
typeOption.timeFormat = timeFormat;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (includeTime != null) {
|
||||||
|
typeOption.includeTime = includeTime;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
emit(state.copyWith(typeOption: newTypeOption));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
class TimestampTypeOptionEvent with _$TimestampTypeOptionEvent {
|
||||||
|
const factory TimestampTypeOptionEvent.didSelectDateFormat(
|
||||||
|
DateFormatPB format,
|
||||||
|
) = _DidSelectDateFormat;
|
||||||
|
const factory TimestampTypeOptionEvent.didSelectTimeFormat(
|
||||||
|
TimeFormatPB format,
|
||||||
|
) = _DidSelectTimeFormat;
|
||||||
|
const factory TimestampTypeOptionEvent.includeTime(bool includeTime) =
|
||||||
|
_IncludeTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
class TimestampTypeOptionState with _$TimestampTypeOptionState {
|
||||||
|
const factory TimestampTypeOptionState({
|
||||||
|
required TimestampTypeOptionPB typeOption,
|
||||||
|
}) = _TimestampTypeOptionState;
|
||||||
|
|
||||||
|
factory TimestampTypeOptionState.initial(TimestampTypeOptionPB typeOption) =>
|
||||||
|
TimestampTypeOptionState(typeOption: typeOption);
|
||||||
|
}
|
@ -4,6 +4,7 @@ import 'package:appflowy_backend/protobuf/flowy-database2/date_entities.pb.dart'
|
|||||||
import 'package:appflowy_backend/protobuf/flowy-database2/number_entities.pb.dart';
|
import 'package:appflowy_backend/protobuf/flowy-database2/number_entities.pb.dart';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-database2/select_option.pb.dart';
|
import 'package:appflowy_backend/protobuf/flowy-database2/select_option.pb.dart';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-database2/text_entities.pb.dart';
|
import 'package:appflowy_backend/protobuf/flowy-database2/text_entities.pb.dart';
|
||||||
|
import 'package:appflowy_backend/protobuf/flowy-database2/timestamp_entities.pb.dart';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-database2/url_entities.pb.dart';
|
import 'package:appflowy_backend/protobuf/flowy-database2/url_entities.pb.dart';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart';
|
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart';
|
||||||
@ -58,7 +59,7 @@ class URLTypeOptionWidgetDataParser extends TypeOptionParser<URLTypeOptionPB> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Date
|
// DateTime
|
||||||
typedef DateTypeOptionContext = TypeOptionContext<DateTypeOptionPB>;
|
typedef DateTypeOptionContext = TypeOptionContext<DateTypeOptionPB>;
|
||||||
|
|
||||||
class DateTypeOptionDataParser extends TypeOptionParser<DateTypeOptionPB> {
|
class DateTypeOptionDataParser extends TypeOptionParser<DateTypeOptionPB> {
|
||||||
@ -68,6 +69,17 @@ class DateTypeOptionDataParser extends TypeOptionParser<DateTypeOptionPB> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// LastModified and CreatedAt
|
||||||
|
typedef TimestampTypeOptionContext = TypeOptionContext<TimestampTypeOptionPB>;
|
||||||
|
|
||||||
|
class TimestampTypeOptionDataParser
|
||||||
|
extends TypeOptionParser<TimestampTypeOptionPB> {
|
||||||
|
@override
|
||||||
|
TimestampTypeOptionPB fromBuffer(List<int> buffer) {
|
||||||
|
return TimestampTypeOptionPB.fromBuffer(buffer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// SingleSelect
|
// SingleSelect
|
||||||
typedef SingleSelectTypeOptionContext
|
typedef SingleSelectTypeOptionContext
|
||||||
= TypeOptionContext<SingleSelectTypeOptionPB>;
|
= TypeOptionContext<SingleSelectTypeOptionPB>;
|
||||||
|
@ -8,6 +8,7 @@ import 'package:appflowy_backend/protobuf/flowy-database2/date_entities.pb.dart'
|
|||||||
import 'package:appflowy_backend/protobuf/flowy-database2/number_entities.pb.dart';
|
import 'package:appflowy_backend/protobuf/flowy-database2/number_entities.pb.dart';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-database2/select_option.pb.dart';
|
import 'package:appflowy_backend/protobuf/flowy-database2/select_option.pb.dart';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-database2/text_entities.pb.dart';
|
import 'package:appflowy_backend/protobuf/flowy-database2/text_entities.pb.dart';
|
||||||
|
import 'package:appflowy_backend/protobuf/flowy-database2/timestamp_entities.pb.dart';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-database2/url_entities.pb.dart';
|
import 'package:appflowy_backend/protobuf/flowy-database2/url_entities.pb.dart';
|
||||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||||
import 'package:protobuf/protobuf.dart' hide FieldInfo;
|
import 'package:protobuf/protobuf.dart' hide FieldInfo;
|
||||||
@ -20,6 +21,7 @@ import 'multi_select.dart';
|
|||||||
import 'number.dart';
|
import 'number.dart';
|
||||||
import 'rich_text.dart';
|
import 'rich_text.dart';
|
||||||
import 'single_select.dart';
|
import 'single_select.dart';
|
||||||
|
import 'timestamp.dart';
|
||||||
import 'url.dart';
|
import 'url.dart';
|
||||||
|
|
||||||
typedef TypeOptionData = Uint8List;
|
typedef TypeOptionData = Uint8List;
|
||||||
@ -73,8 +75,6 @@ TypeOptionWidgetBuilder makeTypeOptionWidgetBuilder({
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
case FieldType.DateTime:
|
case FieldType.DateTime:
|
||||||
case FieldType.LastEditedTime:
|
|
||||||
case FieldType.CreatedTime:
|
|
||||||
return DateTypeOptionWidgetBuilder(
|
return DateTypeOptionWidgetBuilder(
|
||||||
makeTypeOptionContextWithDataController<DateTypeOptionPB>(
|
makeTypeOptionContextWithDataController<DateTypeOptionPB>(
|
||||||
viewId: viewId,
|
viewId: viewId,
|
||||||
@ -83,6 +83,16 @@ TypeOptionWidgetBuilder makeTypeOptionWidgetBuilder({
|
|||||||
),
|
),
|
||||||
popoverMutex,
|
popoverMutex,
|
||||||
);
|
);
|
||||||
|
case FieldType.LastEditedTime:
|
||||||
|
case FieldType.CreatedTime:
|
||||||
|
return TimestampTypeOptionWidgetBuilder(
|
||||||
|
makeTypeOptionContextWithDataController<TimestampTypeOptionPB>(
|
||||||
|
viewId: viewId,
|
||||||
|
fieldType: fieldType,
|
||||||
|
dataController: dataController,
|
||||||
|
),
|
||||||
|
popoverMutex,
|
||||||
|
);
|
||||||
case FieldType.SingleSelect:
|
case FieldType.SingleSelect:
|
||||||
return SingleSelectTypeOptionWidgetBuilder(
|
return SingleSelectTypeOptionWidgetBuilder(
|
||||||
makeTypeOptionContextWithDataController<SingleSelectTypeOptionPB>(
|
makeTypeOptionContextWithDataController<SingleSelectTypeOptionPB>(
|
||||||
@ -203,12 +213,16 @@ TypeOptionContext<T>
|
|||||||
dataParser: CheckboxTypeOptionWidgetDataParser(),
|
dataParser: CheckboxTypeOptionWidgetDataParser(),
|
||||||
) as TypeOptionContext<T>;
|
) as TypeOptionContext<T>;
|
||||||
case FieldType.DateTime:
|
case FieldType.DateTime:
|
||||||
case FieldType.LastEditedTime:
|
|
||||||
case FieldType.CreatedTime:
|
|
||||||
return DateTypeOptionContext(
|
return DateTypeOptionContext(
|
||||||
dataController: dataController,
|
dataController: dataController,
|
||||||
dataParser: DateTypeOptionDataParser(),
|
dataParser: DateTypeOptionDataParser(),
|
||||||
) as TypeOptionContext<T>;
|
) as TypeOptionContext<T>;
|
||||||
|
case FieldType.LastEditedTime:
|
||||||
|
case FieldType.CreatedTime:
|
||||||
|
return TimestampTypeOptionContext(
|
||||||
|
dataController: dataController,
|
||||||
|
dataParser: TimestampTypeOptionDataParser(),
|
||||||
|
) as TypeOptionContext<T>;
|
||||||
case FieldType.SingleSelect:
|
case FieldType.SingleSelect:
|
||||||
return SingleSelectTypeOptionContext(
|
return SingleSelectTypeOptionContext(
|
||||||
dataController: dataController,
|
dataController: dataController,
|
||||||
|
@ -0,0 +1,179 @@
|
|||||||
|
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||||
|
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||||
|
import 'package:appflowy/plugins/database_view/application/field/type_option/timestamp_bloc.dart';
|
||||||
|
import 'package:appflowy/plugins/database_view/application/field/type_option/type_option_context.dart';
|
||||||
|
import 'package:appflowy/plugins/database_view/grid/presentation/layout/sizes.dart';
|
||||||
|
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/common/type_option_separator.dart';
|
||||||
|
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/field_type_option_editor.dart';
|
||||||
|
import 'package:appflowy/workspace/presentation/widgets/toggle/toggle.dart';
|
||||||
|
import 'package:appflowy/workspace/presentation/widgets/toggle/toggle_style.dart';
|
||||||
|
import 'package:appflowy_backend/protobuf/flowy-database2/date_entities.pb.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 'builder.dart';
|
||||||
|
import 'date.dart';
|
||||||
|
|
||||||
|
class TimestampTypeOptionWidgetBuilder extends TypeOptionWidgetBuilder {
|
||||||
|
final TimestampTypeOptionWidget _widget;
|
||||||
|
|
||||||
|
TimestampTypeOptionWidgetBuilder(
|
||||||
|
TimestampTypeOptionContext typeOptionContext,
|
||||||
|
PopoverMutex popoverMutex,
|
||||||
|
) : _widget = TimestampTypeOptionWidget(
|
||||||
|
typeOptionContext: typeOptionContext,
|
||||||
|
popoverMutex: popoverMutex,
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget? build(BuildContext context) {
|
||||||
|
return _widget;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class TimestampTypeOptionWidget extends TypeOptionWidget {
|
||||||
|
final TimestampTypeOptionContext typeOptionContext;
|
||||||
|
final PopoverMutex popoverMutex;
|
||||||
|
const TimestampTypeOptionWidget({
|
||||||
|
required this.typeOptionContext,
|
||||||
|
required this.popoverMutex,
|
||||||
|
Key? key,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return BlocProvider(
|
||||||
|
create: (context) =>
|
||||||
|
TimestampTypeOptionBloc(typeOptionContext: typeOptionContext),
|
||||||
|
child: BlocConsumer<TimestampTypeOptionBloc, TimestampTypeOptionState>(
|
||||||
|
listener: (context, state) =>
|
||||||
|
typeOptionContext.typeOption = state.typeOption,
|
||||||
|
builder: (context, state) {
|
||||||
|
final List<Widget> children = [
|
||||||
|
const TypeOptionSeparator(),
|
||||||
|
_renderDateFormatButton(context, state.typeOption.dateFormat),
|
||||||
|
_renderTimeFormatButton(context, state.typeOption.timeFormat),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 12.0),
|
||||||
|
child: IncludeTimeButton(
|
||||||
|
onChanged: (value) => context
|
||||||
|
.read<TimestampTypeOptionBloc>()
|
||||||
|
.add(TimestampTypeOptionEvent.includeTime(!value)),
|
||||||
|
value: state.typeOption.includeTime,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
return ListView.separated(
|
||||||
|
shrinkWrap: true,
|
||||||
|
separatorBuilder: (context, index) {
|
||||||
|
if (index == 0) {
|
||||||
|
return const SizedBox();
|
||||||
|
} else {
|
||||||
|
return VSpace(GridSize.typeOptionSeparatorHeight);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
itemCount: children.length,
|
||||||
|
itemBuilder: (BuildContext context, int index) => children[index],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _renderDateFormatButton(
|
||||||
|
BuildContext context,
|
||||||
|
DateFormatPB dataFormat,
|
||||||
|
) {
|
||||||
|
return AppFlowyPopover(
|
||||||
|
mutex: popoverMutex,
|
||||||
|
asBarrier: true,
|
||||||
|
triggerActions: PopoverTriggerFlags.hover | PopoverTriggerFlags.click,
|
||||||
|
offset: const Offset(8, 0),
|
||||||
|
constraints: BoxConstraints.loose(const Size(460, 440)),
|
||||||
|
popupBuilder: (popoverContext) {
|
||||||
|
return DateFormatList(
|
||||||
|
selectedFormat: dataFormat,
|
||||||
|
onSelected: (format) {
|
||||||
|
context
|
||||||
|
.read<TimestampTypeOptionBloc>()
|
||||||
|
.add(TimestampTypeOptionEvent.didSelectDateFormat(format));
|
||||||
|
PopoverContainer.of(popoverContext).close();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: const Padding(
|
||||||
|
padding: EdgeInsets.symmetric(horizontal: 12.0),
|
||||||
|
child: DateFormatButton(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _renderTimeFormatButton(
|
||||||
|
BuildContext context,
|
||||||
|
TimeFormatPB timeFormat,
|
||||||
|
) {
|
||||||
|
return AppFlowyPopover(
|
||||||
|
mutex: popoverMutex,
|
||||||
|
asBarrier: true,
|
||||||
|
triggerActions: PopoverTriggerFlags.hover | PopoverTriggerFlags.click,
|
||||||
|
offset: const Offset(8, 0),
|
||||||
|
constraints: BoxConstraints.loose(const Size(460, 440)),
|
||||||
|
popupBuilder: (BuildContext popoverContext) {
|
||||||
|
return TimeFormatList(
|
||||||
|
selectedFormat: timeFormat,
|
||||||
|
onSelected: (format) {
|
||||||
|
context
|
||||||
|
.read<TimestampTypeOptionBloc>()
|
||||||
|
.add(TimestampTypeOptionEvent.didSelectTimeFormat(format));
|
||||||
|
PopoverContainer.of(popoverContext).close();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 12.0),
|
||||||
|
child: TimeFormatButton(timeFormat: timeFormat),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class IncludeTimeButton extends StatelessWidget {
|
||||||
|
final bool value;
|
||||||
|
final Function(bool value) onChanged;
|
||||||
|
const IncludeTimeButton({
|
||||||
|
super.key,
|
||||||
|
required this.onChanged,
|
||||||
|
required this.value,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return SizedBox(
|
||||||
|
height: GridSize.popoverItemHeight,
|
||||||
|
child: Padding(
|
||||||
|
padding: GridSize.typeOptionContentInsets,
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
FlowySvg(
|
||||||
|
FlowySvgs.clock_alarm_s,
|
||||||
|
color: Theme.of(context).iconTheme.color,
|
||||||
|
),
|
||||||
|
const HSpace(6),
|
||||||
|
FlowyText.medium(LocaleKeys.grid_field_includeTime.tr()),
|
||||||
|
const Spacer(),
|
||||||
|
Toggle(
|
||||||
|
value: value,
|
||||||
|
onChanged: onChanged,
|
||||||
|
style: ToggleStyle.big,
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,80 @@
|
|||||||
|
import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart';
|
||||||
|
import 'package:appflowy/plugins/database_view/application/field/field_info.dart';
|
||||||
|
import 'package:appflowy_backend/protobuf/flowy-database2/timestamp_entities.pb.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
part 'timestamp_card_cell_bloc.freezed.dart';
|
||||||
|
|
||||||
|
class TimestampCardCellBloc
|
||||||
|
extends Bloc<TimestampCardCellEvent, TimestampCardCellState> {
|
||||||
|
final TimestampCellController cellController;
|
||||||
|
void Function()? _onCellChangedFn;
|
||||||
|
|
||||||
|
TimestampCardCellBloc({required this.cellController})
|
||||||
|
: super(TimestampCardCellState.initial(cellController)) {
|
||||||
|
on<TimestampCardCellEvent>(
|
||||||
|
(event, emit) async {
|
||||||
|
event.when(
|
||||||
|
initial: () => _startListening(),
|
||||||
|
didReceiveCellUpdate: (TimestampCellDataPB? cellData) {
|
||||||
|
emit(
|
||||||
|
state.copyWith(
|
||||||
|
data: cellData,
|
||||||
|
dateStr: cellData?.dateTime ?? "",
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> close() async {
|
||||||
|
if (_onCellChangedFn != null) {
|
||||||
|
cellController.removeListener(_onCellChangedFn!);
|
||||||
|
_onCellChangedFn = null;
|
||||||
|
}
|
||||||
|
await cellController.dispose();
|
||||||
|
return super.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _startListening() {
|
||||||
|
_onCellChangedFn = cellController.startListening(
|
||||||
|
onCellChanged: ((data) {
|
||||||
|
if (!isClosed) {
|
||||||
|
add(TimestampCardCellEvent.didReceiveCellUpdate(data));
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
class TimestampCardCellEvent with _$TimestampCardCellEvent {
|
||||||
|
const factory TimestampCardCellEvent.initial() = _InitialCell;
|
||||||
|
const factory TimestampCardCellEvent.didReceiveCellUpdate(
|
||||||
|
TimestampCellDataPB? data,
|
||||||
|
) = _DidReceiveCellUpdate;
|
||||||
|
}
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
class TimestampCardCellState with _$TimestampCardCellState {
|
||||||
|
const factory TimestampCardCellState({
|
||||||
|
required TimestampCellDataPB? data,
|
||||||
|
required String dateStr,
|
||||||
|
required FieldInfo fieldInfo,
|
||||||
|
}) = _TimestampCardCellState;
|
||||||
|
|
||||||
|
factory TimestampCardCellState.initial(TimestampCellController context) {
|
||||||
|
final cellData = context.getCellData();
|
||||||
|
|
||||||
|
return TimestampCardCellState(
|
||||||
|
fieldInfo: context.fieldInfo,
|
||||||
|
data: cellData,
|
||||||
|
dateStr: cellData?.dateTime ?? "",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -10,6 +10,7 @@ import 'cells/date_card_cell.dart';
|
|||||||
import 'cells/number_card_cell.dart';
|
import 'cells/number_card_cell.dart';
|
||||||
import 'cells/select_option_card_cell.dart';
|
import 'cells/select_option_card_cell.dart';
|
||||||
import 'cells/text_card_cell.dart';
|
import 'cells/text_card_cell.dart';
|
||||||
|
import 'cells/timestamp_card_cell.dart';
|
||||||
import 'cells/url_card_cell.dart';
|
import 'cells/url_card_cell.dart';
|
||||||
|
|
||||||
// T represents as the Generic card data
|
// T represents as the Generic card data
|
||||||
@ -39,13 +40,23 @@ class CardCellBuilder<CustomCardData> {
|
|||||||
key: key,
|
key: key,
|
||||||
);
|
);
|
||||||
case FieldType.DateTime:
|
case FieldType.DateTime:
|
||||||
case FieldType.LastEditedTime:
|
|
||||||
case FieldType.CreatedTime:
|
|
||||||
return DateCardCell<CustomCardData>(
|
return DateCardCell<CustomCardData>(
|
||||||
renderHook: renderHook?.renderHook[FieldType.DateTime],
|
renderHook: renderHook?.renderHook[FieldType.DateTime],
|
||||||
cellControllerBuilder: cellControllerBuilder,
|
cellControllerBuilder: cellControllerBuilder,
|
||||||
key: key,
|
key: key,
|
||||||
);
|
);
|
||||||
|
case FieldType.LastEditedTime:
|
||||||
|
return TimestampCardCell<CustomCardData>(
|
||||||
|
renderHook: renderHook?.renderHook[FieldType.LastEditedTime],
|
||||||
|
cellControllerBuilder: cellControllerBuilder,
|
||||||
|
key: key,
|
||||||
|
);
|
||||||
|
case FieldType.CreatedTime:
|
||||||
|
return TimestampCardCell<CustomCardData>(
|
||||||
|
renderHook: renderHook?.renderHook[FieldType.CreatedTime],
|
||||||
|
cellControllerBuilder: cellControllerBuilder,
|
||||||
|
key: key,
|
||||||
|
);
|
||||||
case FieldType.SingleSelect:
|
case FieldType.SingleSelect:
|
||||||
return SelectOptionCardCell<CustomCardData>(
|
return SelectOptionCardCell<CustomCardData>(
|
||||||
renderHook: renderHook?.renderHook[FieldType.SingleSelect],
|
renderHook: renderHook?.renderHook[FieldType.SingleSelect],
|
||||||
|
@ -0,0 +1,79 @@
|
|||||||
|
import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart';
|
||||||
|
import 'package:appflowy/plugins/database_view/widgets/card/bloc/timestamp_card_cell_bloc.dart';
|
||||||
|
import 'package:flowy_infra_ui/style_widget/text.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
|
||||||
|
import '../define.dart';
|
||||||
|
import 'card_cell.dart';
|
||||||
|
|
||||||
|
class TimestampCardCell<CustomCardData> extends CardCell {
|
||||||
|
final CellControllerBuilder cellControllerBuilder;
|
||||||
|
final CellRenderHook<dynamic, CustomCardData>? renderHook;
|
||||||
|
|
||||||
|
const TimestampCardCell({
|
||||||
|
required this.cellControllerBuilder,
|
||||||
|
this.renderHook,
|
||||||
|
Key? key,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<TimestampCardCell> createState() => _TimestampCardCellState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _TimestampCardCellState extends State<TimestampCardCell> {
|
||||||
|
late TimestampCardCellBloc _cellBloc;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
final cellController =
|
||||||
|
widget.cellControllerBuilder.build() as TimestampCellController;
|
||||||
|
|
||||||
|
_cellBloc = TimestampCardCellBloc(cellController: cellController)
|
||||||
|
..add(const TimestampCardCellEvent.initial());
|
||||||
|
super.initState();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return BlocProvider.value(
|
||||||
|
value: _cellBloc,
|
||||||
|
child: BlocBuilder<TimestampCardCellBloc, TimestampCardCellState>(
|
||||||
|
buildWhen: (previous, current) => previous.dateStr != current.dateStr,
|
||||||
|
builder: (context, state) {
|
||||||
|
if (state.dateStr.isEmpty) {
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
}
|
||||||
|
final Widget? custom = widget.renderHook?.call(
|
||||||
|
state.data,
|
||||||
|
widget.cardData,
|
||||||
|
context,
|
||||||
|
);
|
||||||
|
if (custom != null) {
|
||||||
|
return custom;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Align(
|
||||||
|
alignment: Alignment.centerLeft,
|
||||||
|
child: Padding(
|
||||||
|
padding: EdgeInsets.symmetric(
|
||||||
|
vertical: CardSizes.cardCellVPadding,
|
||||||
|
),
|
||||||
|
child: FlowyText.regular(
|
||||||
|
state.dateStr,
|
||||||
|
fontSize: 13,
|
||||||
|
color: Theme.of(context).hintColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> dispose() async {
|
||||||
|
_cellBloc.close();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
}
|
@ -11,6 +11,7 @@ import 'cells/date_cell/date_cell.dart';
|
|||||||
import 'cells/number_cell/number_cell.dart';
|
import 'cells/number_cell/number_cell.dart';
|
||||||
import 'cells/select_option_cell/select_option_cell.dart';
|
import 'cells/select_option_cell/select_option_cell.dart';
|
||||||
import 'cells/text_cell/text_cell.dart';
|
import 'cells/text_cell/text_cell.dart';
|
||||||
|
import 'cells/timestamp_cell/timestamp_cell.dart';
|
||||||
import 'cells/url_cell/url_cell.dart';
|
import 'cells/url_cell/url_cell.dart';
|
||||||
|
|
||||||
/// Build the cell widget in Grid style.
|
/// Build the cell widget in Grid style.
|
||||||
@ -41,14 +42,12 @@ class GridCellBuilder {
|
|||||||
cellControllerBuilder: cellControllerBuilder,
|
cellControllerBuilder: cellControllerBuilder,
|
||||||
key: key,
|
key: key,
|
||||||
style: style,
|
style: style,
|
||||||
fieldType: cellContext.fieldType,
|
|
||||||
);
|
);
|
||||||
case FieldType.LastEditedTime:
|
case FieldType.LastEditedTime:
|
||||||
case FieldType.CreatedTime:
|
case FieldType.CreatedTime:
|
||||||
return GridDateCell(
|
return GridTimestampCell(
|
||||||
cellControllerBuilder: cellControllerBuilder,
|
cellControllerBuilder: cellControllerBuilder,
|
||||||
key: key,
|
key: key,
|
||||||
editable: false,
|
|
||||||
style: style,
|
style: style,
|
||||||
fieldType: cellContext.fieldType,
|
fieldType: cellContext.fieldType,
|
||||||
);
|
);
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
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_backend/protobuf/flowy-database2/field_entities.pbenum.dart';
|
|
||||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
@ -22,20 +21,12 @@ abstract class GridCellDelegate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class GridDateCell extends GridCellWidget {
|
class GridDateCell extends GridCellWidget {
|
||||||
final bool editable;
|
|
||||||
|
|
||||||
/// The [GridDateCell] is used by Field Type [FieldType.DateTime],
|
|
||||||
/// [FieldType.CreatedTime], [FieldType.LastEditedTime]. So it needs
|
|
||||||
/// to know the field type.
|
|
||||||
final FieldType fieldType;
|
|
||||||
final CellControllerBuilder cellControllerBuilder;
|
final CellControllerBuilder cellControllerBuilder;
|
||||||
late final DateCellStyle? cellStyle;
|
late final DateCellStyle? cellStyle;
|
||||||
|
|
||||||
GridDateCell({
|
GridDateCell({
|
||||||
GridCellStyle? style,
|
GridCellStyle? style,
|
||||||
required this.fieldType,
|
|
||||||
required this.cellControllerBuilder,
|
required this.cellControllerBuilder,
|
||||||
this.editable = true,
|
|
||||||
Key? key,
|
Key? key,
|
||||||
}) : super(key: key) {
|
}) : super(key: key) {
|
||||||
if (style != null) {
|
if (style != null) {
|
||||||
@ -72,24 +63,20 @@ class _DateCellState extends GridCellState<GridDateCell> {
|
|||||||
value: _cellBloc,
|
value: _cellBloc,
|
||||||
child: BlocBuilder<DateCellBloc, DateCellState>(
|
child: BlocBuilder<DateCellBloc, DateCellState>(
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
Widget dateTextWidget = GridDateCellText(
|
return AppFlowyPopover(
|
||||||
dateStr: state.dateStr,
|
|
||||||
alignment: alignment,
|
|
||||||
);
|
|
||||||
|
|
||||||
// If the cell is editable, wrap it in a popover.
|
|
||||||
if (widget.editable) {
|
|
||||||
dateTextWidget = AppFlowyPopover(
|
|
||||||
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, 520)),
|
||||||
margin: EdgeInsets.zero,
|
margin: EdgeInsets.zero,
|
||||||
child: dateTextWidget,
|
child: GridDateCellText(
|
||||||
|
dateStr: state.dateStr,
|
||||||
|
alignment: alignment,
|
||||||
|
),
|
||||||
popupBuilder: (BuildContext popoverContent) {
|
popupBuilder: (BuildContext popoverContent) {
|
||||||
return DateCellEditor(
|
return DateCellEditor(
|
||||||
cellController: widget.cellControllerBuilder.build()
|
cellController:
|
||||||
as DateCellController,
|
widget.cellControllerBuilder.build() as DateCellController,
|
||||||
onDismissed: () => widget.onCellFocus.value = false,
|
onDismissed: () => widget.onCellFocus.value = false,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@ -97,8 +84,6 @@ class _DateCellState extends GridCellState<GridDateCell> {
|
|||||||
widget.onCellFocus.value = false;
|
widget.onCellFocus.value = false;
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
|
||||||
return dateTextWidget;
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@ -113,11 +98,8 @@ class _DateCellState extends GridCellState<GridDateCell> {
|
|||||||
@override
|
@override
|
||||||
void requestBeginFocus() {
|
void requestBeginFocus() {
|
||||||
_popover.show();
|
_popover.show();
|
||||||
|
|
||||||
if (widget.editable) {
|
|
||||||
widget.onCellFocus.value = true;
|
widget.onCellFocus.value = true;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String? onCopy() => _cellBloc.state.dateStr;
|
String? onCopy() => _cellBloc.state.dateStr;
|
||||||
|
@ -2,8 +2,7 @@ import 'package:appflowy/generated/flowy_svgs.g.dart';
|
|||||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||||
import 'package:appflowy/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/workspace/presentation/widgets/toggle/toggle.dart';
|
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/type_option/timestamp.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';
|
||||||
@ -241,30 +240,11 @@ class _IncludeTimeButton extends StatelessWidget {
|
|||||||
builder: (context, includeTime) {
|
builder: (context, includeTime) {
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 12.0),
|
padding: const EdgeInsets.symmetric(horizontal: 12.0),
|
||||||
child: SizedBox(
|
child: IncludeTimeButton(
|
||||||
height: GridSize.popoverItemHeight,
|
|
||||||
child: Padding(
|
|
||||||
padding: GridSize.typeOptionContentInsets,
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
FlowySvg(
|
|
||||||
FlowySvgs.clock_alarm_s,
|
|
||||||
color: Theme.of(context).iconTheme.color,
|
|
||||||
),
|
|
||||||
const HSpace(6),
|
|
||||||
FlowyText.medium(LocaleKeys.grid_field_includeTime.tr()),
|
|
||||||
const Spacer(),
|
|
||||||
Toggle(
|
|
||||||
value: includeTime,
|
|
||||||
onChanged: (value) => context
|
onChanged: (value) => context
|
||||||
.read<DateCellCalendarBloc>()
|
.read<DateCellCalendarBloc>()
|
||||||
.add(DateCellCalendarEvent.setIncludeTime(!value)),
|
.add(DateCellCalendarEvent.setIncludeTime(!value)),
|
||||||
style: ToggleStyle.big,
|
value: includeTime,
|
||||||
padding: EdgeInsets.zero,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
@ -0,0 +1,109 @@
|
|||||||
|
import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart';
|
||||||
|
import 'package:appflowy/plugins/database_view/grid/presentation/layout/sizes.dart';
|
||||||
|
import 'package:appflowy/plugins/database_view/widgets/row/cell_builder.dart';
|
||||||
|
import 'package:appflowy/plugins/database_view/widgets/row/cells/timestamp_cell/timestamp_cell_bloc.dart';
|
||||||
|
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart';
|
||||||
|
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
|
||||||
|
class TimestampCellStyle extends GridCellStyle {
|
||||||
|
Alignment alignment;
|
||||||
|
|
||||||
|
TimestampCellStyle({this.alignment = Alignment.center});
|
||||||
|
}
|
||||||
|
|
||||||
|
class GridTimestampCell extends GridCellWidget {
|
||||||
|
/// The [GridTimestampCell] is used by both [FieldType.CreatedTime]
|
||||||
|
/// and [FieldType.LastEditedTime]. So it needs to know the field type.
|
||||||
|
final FieldType fieldType;
|
||||||
|
final CellControllerBuilder cellControllerBuilder;
|
||||||
|
late final TimestampCellStyle? cellStyle;
|
||||||
|
|
||||||
|
GridTimestampCell({
|
||||||
|
GridCellStyle? style,
|
||||||
|
required this.fieldType,
|
||||||
|
required this.cellControllerBuilder,
|
||||||
|
Key? key,
|
||||||
|
}) : super(key: key) {
|
||||||
|
if (style != null) {
|
||||||
|
cellStyle = (style as TimestampCellStyle);
|
||||||
|
} else {
|
||||||
|
cellStyle = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
GridCellState<GridTimestampCell> createState() => _TimestampCellState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _TimestampCellState extends GridCellState<GridTimestampCell> {
|
||||||
|
late TimestampCellBloc _cellBloc;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
final cellController =
|
||||||
|
widget.cellControllerBuilder.build() as TimestampCellController;
|
||||||
|
_cellBloc = TimestampCellBloc(cellController: cellController)
|
||||||
|
..add(const TimestampCellEvent.initial());
|
||||||
|
super.initState();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final alignment = widget.cellStyle != null
|
||||||
|
? widget.cellStyle!.alignment
|
||||||
|
: Alignment.centerLeft;
|
||||||
|
return BlocProvider.value(
|
||||||
|
value: _cellBloc,
|
||||||
|
child: BlocBuilder<TimestampCellBloc, TimestampCellState>(
|
||||||
|
builder: (context, state) {
|
||||||
|
return GridTimestampCellText(
|
||||||
|
dateStr: state.dateStr,
|
||||||
|
alignment: alignment,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> dispose() async {
|
||||||
|
_cellBloc.close();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String? onCopy() => _cellBloc.state.dateStr;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void requestBeginFocus() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class GridTimestampCellText extends StatelessWidget {
|
||||||
|
final String dateStr;
|
||||||
|
final Alignment alignment;
|
||||||
|
const GridTimestampCellText({
|
||||||
|
required this.dateStr,
|
||||||
|
required this.alignment,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return SizedBox.expand(
|
||||||
|
child: Align(
|
||||||
|
alignment: alignment,
|
||||||
|
child: Padding(
|
||||||
|
padding: GridSize.cellContentInsets,
|
||||||
|
child: FlowyText.medium(
|
||||||
|
dateStr,
|
||||||
|
maxLines: null,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,80 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart';
|
||||||
|
import 'package:appflowy/plugins/database_view/application/field/field_info.dart';
|
||||||
|
import 'package:appflowy_backend/protobuf/flowy-database2/timestamp_entities.pb.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
|
|
||||||
|
part 'timestamp_cell_bloc.freezed.dart';
|
||||||
|
|
||||||
|
class TimestampCellBloc extends Bloc<TimestampCellEvent, TimestampCellState> {
|
||||||
|
final TimestampCellController cellController;
|
||||||
|
void Function()? _onCellChangedFn;
|
||||||
|
|
||||||
|
TimestampCellBloc({required this.cellController})
|
||||||
|
: super(TimestampCellState.initial(cellController)) {
|
||||||
|
on<TimestampCellEvent>(
|
||||||
|
(event, emit) async {
|
||||||
|
event.when(
|
||||||
|
initial: () => _startListening(),
|
||||||
|
didReceiveCellUpdate: (TimestampCellDataPB? cellData) {
|
||||||
|
emit(
|
||||||
|
state.copyWith(
|
||||||
|
data: cellData,
|
||||||
|
dateStr: cellData?.dateTime ?? "",
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> close() async {
|
||||||
|
if (_onCellChangedFn != null) {
|
||||||
|
cellController.removeListener(_onCellChangedFn!);
|
||||||
|
_onCellChangedFn = null;
|
||||||
|
}
|
||||||
|
await cellController.dispose();
|
||||||
|
return super.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _startListening() {
|
||||||
|
_onCellChangedFn = cellController.startListening(
|
||||||
|
onCellChanged: ((data) {
|
||||||
|
if (!isClosed) {
|
||||||
|
add(TimestampCellEvent.didReceiveCellUpdate(data));
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
class TimestampCellEvent with _$TimestampCellEvent {
|
||||||
|
const factory TimestampCellEvent.initial() = _InitialCell;
|
||||||
|
const factory TimestampCellEvent.didReceiveCellUpdate(
|
||||||
|
TimestampCellDataPB? data,
|
||||||
|
) = _DidReceiveCellUpdate;
|
||||||
|
}
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
class TimestampCellState with _$TimestampCellState {
|
||||||
|
const factory TimestampCellState({
|
||||||
|
required TimestampCellDataPB? data,
|
||||||
|
required String dateStr,
|
||||||
|
required FieldInfo fieldInfo,
|
||||||
|
}) = _TimestampCellState;
|
||||||
|
|
||||||
|
factory TimestampCellState.initial(TimestampCellController context) {
|
||||||
|
final cellData = context.getCellData();
|
||||||
|
|
||||||
|
return TimestampCellState(
|
||||||
|
fieldInfo: context.fieldInfo,
|
||||||
|
data: cellData,
|
||||||
|
dateStr: cellData?.dateTime ?? "",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -18,6 +18,7 @@ import 'cell_builder.dart';
|
|||||||
import 'cells/date_cell/date_cell.dart';
|
import 'cells/date_cell/date_cell.dart';
|
||||||
import 'cells/select_option_cell/select_option_cell.dart';
|
import 'cells/select_option_cell/select_option_cell.dart';
|
||||||
import 'cells/text_cell/text_cell.dart';
|
import 'cells/text_cell/text_cell.dart';
|
||||||
|
import 'cells/timestamp_cell/timestamp_cell.dart';
|
||||||
import 'cells/url_cell/url_cell.dart';
|
import 'cells/url_cell/url_cell.dart';
|
||||||
|
|
||||||
/// Display the row properties in a list. Only use this widget in the
|
/// Display the row properties in a list. Only use this widget in the
|
||||||
@ -156,9 +157,12 @@ GridCellStyle? _customCellStyle(FieldType fieldType) {
|
|||||||
case FieldType.Checkbox:
|
case FieldType.Checkbox:
|
||||||
return null;
|
return null;
|
||||||
case FieldType.DateTime:
|
case FieldType.DateTime:
|
||||||
|
return DateCellStyle(
|
||||||
|
alignment: Alignment.centerLeft,
|
||||||
|
);
|
||||||
case FieldType.LastEditedTime:
|
case FieldType.LastEditedTime:
|
||||||
case FieldType.CreatedTime:
|
case FieldType.CreatedTime:
|
||||||
return DateCellStyle(
|
return TimestampCellStyle(
|
||||||
alignment: Alignment.centerLeft,
|
alignment: Alignment.centerLeft,
|
||||||
);
|
);
|
||||||
case FieldType.MultiSelect:
|
case FieldType.MultiSelect:
|
||||||
|
@ -4,7 +4,7 @@ use strum_macros::EnumIter;
|
|||||||
|
|
||||||
use flowy_derive::{ProtoBuf, ProtoBuf_Enum};
|
use flowy_derive::{ProtoBuf, ProtoBuf_Enum};
|
||||||
|
|
||||||
use crate::entities::{CellIdPB, FieldType};
|
use crate::entities::CellIdPB;
|
||||||
use crate::services::field::{DateFormat, DateTypeOption, TimeFormat};
|
use crate::services::field::{DateFormat, DateTypeOption, TimeFormat};
|
||||||
|
|
||||||
#[derive(Clone, Debug, Default, ProtoBuf)]
|
#[derive(Clone, Debug, Default, ProtoBuf)]
|
||||||
@ -51,9 +51,6 @@ pub struct DateTypeOptionPB {
|
|||||||
|
|
||||||
#[pb(index = 3)]
|
#[pb(index = 3)]
|
||||||
pub timezone_id: String,
|
pub timezone_id: String,
|
||||||
|
|
||||||
#[pb(index = 4)]
|
|
||||||
pub field_type: FieldType,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<DateTypeOption> for DateTypeOptionPB {
|
impl From<DateTypeOption> for DateTypeOptionPB {
|
||||||
@ -62,7 +59,6 @@ impl From<DateTypeOption> for DateTypeOptionPB {
|
|||||||
date_format: data.date_format.into(),
|
date_format: data.date_format.into(),
|
||||||
time_format: data.time_format.into(),
|
time_format: data.time_format.into(),
|
||||||
timezone_id: data.timezone_id,
|
timezone_id: data.timezone_id,
|
||||||
field_type: data.field_type,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -73,7 +69,6 @@ impl From<DateTypeOptionPB> for DateTypeOption {
|
|||||||
date_format: data.date_format.into(),
|
date_format: data.date_format.into(),
|
||||||
time_format: data.time_format.into(),
|
time_format: data.time_format.into(),
|
||||||
timezone_id: data.timezone_id,
|
timezone_id: data.timezone_id,
|
||||||
field_type: data.field_type,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,6 +4,7 @@ mod date_entities;
|
|||||||
mod number_entities;
|
mod number_entities;
|
||||||
mod select_option;
|
mod select_option;
|
||||||
mod text_entities;
|
mod text_entities;
|
||||||
|
mod timestamp_entities;
|
||||||
mod url_entities;
|
mod url_entities;
|
||||||
|
|
||||||
pub use checkbox_entities::*;
|
pub use checkbox_entities::*;
|
||||||
@ -12,4 +13,5 @@ pub use date_entities::*;
|
|||||||
pub use number_entities::*;
|
pub use number_entities::*;
|
||||||
pub use select_option::*;
|
pub use select_option::*;
|
||||||
pub use text_entities::*;
|
pub use text_entities::*;
|
||||||
|
pub use timestamp_entities::*;
|
||||||
pub use url_entities::*;
|
pub use url_entities::*;
|
||||||
|
@ -0,0 +1,50 @@
|
|||||||
|
use flowy_derive::ProtoBuf;
|
||||||
|
|
||||||
|
use crate::entities::{DateFormatPB, FieldType, TimeFormatPB};
|
||||||
|
use crate::services::field::TimestampTypeOption;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Default, ProtoBuf)]
|
||||||
|
pub struct TimestampCellDataPB {
|
||||||
|
#[pb(index = 1)]
|
||||||
|
pub date_time: String,
|
||||||
|
|
||||||
|
#[pb(index = 2, one_of)]
|
||||||
|
pub timestamp: Option<i64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Default, ProtoBuf)]
|
||||||
|
pub struct TimestampTypeOptionPB {
|
||||||
|
#[pb(index = 1)]
|
||||||
|
pub date_format: DateFormatPB,
|
||||||
|
|
||||||
|
#[pb(index = 2)]
|
||||||
|
pub time_format: TimeFormatPB,
|
||||||
|
|
||||||
|
#[pb(index = 3)]
|
||||||
|
pub include_time: bool,
|
||||||
|
|
||||||
|
#[pb(index = 4)]
|
||||||
|
pub field_type: FieldType,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<TimestampTypeOption> for TimestampTypeOptionPB {
|
||||||
|
fn from(data: TimestampTypeOption) -> Self {
|
||||||
|
Self {
|
||||||
|
date_format: data.date_format.into(),
|
||||||
|
time_format: data.time_format.into(),
|
||||||
|
include_time: data.include_time,
|
||||||
|
field_type: data.field_type,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<TimestampTypeOptionPB> for TimestampTypeOption {
|
||||||
|
fn from(data: TimestampTypeOptionPB) -> Self {
|
||||||
|
Self {
|
||||||
|
date_format: data.date_format.into(),
|
||||||
|
time_format: data.time_format.into(),
|
||||||
|
include_time: data.include_time,
|
||||||
|
field_type: data.field_type,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -24,8 +24,8 @@ use crate::services::database_view::{DatabaseViewChanged, DatabaseViewData, Data
|
|||||||
use crate::services::field::checklist_type_option::{ChecklistCellChangeset, ChecklistCellData};
|
use crate::services::field::checklist_type_option::{ChecklistCellChangeset, ChecklistCellData};
|
||||||
use crate::services::field::{
|
use crate::services::field::{
|
||||||
default_type_option_data_from_type, select_type_option_from_field, transform_type_option,
|
default_type_option_data_from_type, select_type_option_from_field, transform_type_option,
|
||||||
type_option_data_from_pb_or_default, type_option_to_pb, DateCellData, SelectOptionCellChangeset,
|
type_option_data_from_pb_or_default, type_option_to_pb, SelectOptionCellChangeset,
|
||||||
SelectOptionIds, TypeOptionCellDataHandler, TypeOptionCellExt,
|
SelectOptionIds, TimestampCellData, TypeOptionCellDataHandler, TypeOptionCellExt,
|
||||||
};
|
};
|
||||||
use crate::services::field_settings::{
|
use crate::services::field_settings::{
|
||||||
default_field_settings_by_layout_map, FieldSettings, FieldSettingsChangesetParams,
|
default_field_settings_by_layout_map, FieldSettings, FieldSettingsChangesetParams,
|
||||||
@ -614,9 +614,9 @@ impl DatabaseEditor {
|
|||||||
FieldType::LastEditedTime | FieldType::CreatedTime => {
|
FieldType::LastEditedTime | FieldType::CreatedTime => {
|
||||||
let row = database.get_row(row_id);
|
let row = database.get_row(row_id);
|
||||||
let cell_data = if field_type.is_created_time() {
|
let cell_data = if field_type.is_created_time() {
|
||||||
DateCellData::new(row.created_at, true)
|
TimestampCellData::new(row.created_at)
|
||||||
} else {
|
} else {
|
||||||
DateCellData::new(row.modified_at, true)
|
TimestampCellData::new(row.modified_at)
|
||||||
};
|
};
|
||||||
Some(Cell::from(cell_data))
|
Some(Cell::from(cell_data))
|
||||||
},
|
},
|
||||||
@ -651,9 +651,9 @@ impl DatabaseEditor {
|
|||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|row| {
|
.map(|row| {
|
||||||
let data = if field_type.is_created_time() {
|
let data = if field_type.is_created_time() {
|
||||||
DateCellData::new(row.created_at, true)
|
TimestampCellData::new(row.created_at)
|
||||||
} else {
|
} else {
|
||||||
DateCellData::new(row.modified_at, true)
|
TimestampCellData::new(row.modified_at)
|
||||||
};
|
};
|
||||||
RowCell {
|
RowCell {
|
||||||
row_id: row.id,
|
row_id: row.id,
|
||||||
|
@ -10,7 +10,6 @@ mod tests {
|
|||||||
use crate::services::cell::{CellDataChangeset, CellDataDecoder};
|
use crate::services::cell::{CellDataChangeset, CellDataDecoder};
|
||||||
use crate::services::field::{
|
use crate::services::field::{
|
||||||
DateCellChangeset, DateFormat, DateTypeOption, FieldBuilder, TimeFormat,
|
DateCellChangeset, DateFormat, DateTypeOption, FieldBuilder, TimeFormat,
|
||||||
TypeOptionCellDataSerde,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@ -409,33 +408,18 @@ mod tests {
|
|||||||
old_cell_data: Option<Cell>,
|
old_cell_data: Option<Cell>,
|
||||||
expected_str: &str,
|
expected_str: &str,
|
||||||
) {
|
) {
|
||||||
let (cell, cell_data) = type_option
|
let (cell, _) = type_option
|
||||||
.apply_changeset(changeset, old_cell_data)
|
.apply_changeset(changeset, old_cell_data)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(decode_cell_data(&cell, type_option, field), expected_str,);
|
||||||
decode_cell_data(&cell, type_option, cell_data.include_time, field),
|
|
||||||
expected_str,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn decode_cell_data(
|
fn decode_cell_data(cell: &Cell, type_option: &DateTypeOption, field: &Field) -> String {
|
||||||
cell: &Cell,
|
|
||||||
type_option: &DateTypeOption,
|
|
||||||
include_time: bool,
|
|
||||||
field: &Field,
|
|
||||||
) -> String {
|
|
||||||
let decoded_data = type_option
|
let decoded_data = type_option
|
||||||
.decode_cell(cell, &FieldType::DateTime, field)
|
.decode_cell(cell, &FieldType::DateTime, field)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let decoded_data = type_option.protobuf_encode(decoded_data);
|
type_option.stringify_cell_data(decoded_data)
|
||||||
if include_time {
|
|
||||||
format!("{} {}", decoded_data.date, decoded_data.time)
|
|
||||||
.trim_end()
|
|
||||||
.to_owned()
|
|
||||||
} else {
|
|
||||||
decoded_data.date
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn initialize_date_cell(type_option: &DateTypeOption, changeset: DateCellChangeset) -> Cell {
|
fn initialize_date_cell(type_option: &DateTypeOption, changeset: DateCellChangeset) -> Cell {
|
||||||
|
@ -13,8 +13,8 @@ use flowy_error::{ErrorCode, FlowyError, FlowyResult};
|
|||||||
use crate::entities::{DateCellDataPB, DateFilterPB, FieldType};
|
use crate::entities::{DateCellDataPB, DateFilterPB, FieldType};
|
||||||
use crate::services::cell::{CellDataChangeset, CellDataDecoder};
|
use crate::services::cell::{CellDataChangeset, CellDataDecoder};
|
||||||
use crate::services::field::{
|
use crate::services::field::{
|
||||||
default_order, DateCellChangeset, DateCellData, DateCellDataWrapper, DateFormat, TimeFormat,
|
default_order, DateCellChangeset, DateCellData, DateFormat, TimeFormat, TypeOption,
|
||||||
TypeOption, TypeOptionCellDataCompare, TypeOptionCellDataFilter, TypeOptionCellDataSerde,
|
TypeOptionCellDataCompare, TypeOptionCellDataFilter, TypeOptionCellDataSerde,
|
||||||
TypeOptionTransform,
|
TypeOptionTransform,
|
||||||
};
|
};
|
||||||
use crate::services::sort::SortCondition;
|
use crate::services::sort::SortCondition;
|
||||||
@ -27,7 +27,6 @@ pub struct DateTypeOption {
|
|||||||
pub date_format: DateFormat,
|
pub date_format: DateFormat,
|
||||||
pub time_format: TimeFormat,
|
pub time_format: TimeFormat,
|
||||||
pub timezone_id: String,
|
pub timezone_id: String,
|
||||||
pub field_type: FieldType,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for DateTypeOption {
|
impl Default for DateTypeOption {
|
||||||
@ -36,7 +35,6 @@ impl Default for DateTypeOption {
|
|||||||
date_format: Default::default(),
|
date_format: Default::default(),
|
||||||
time_format: Default::default(),
|
time_format: Default::default(),
|
||||||
timezone_id: Default::default(),
|
timezone_id: Default::default(),
|
||||||
field_type: FieldType::DateTime,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -59,15 +57,10 @@ impl From<TypeOptionData> for DateTypeOption {
|
|||||||
.map(TimeFormat::from)
|
.map(TimeFormat::from)
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
let timezone_id = data.get_str_value("timezone_id").unwrap_or_default();
|
let timezone_id = data.get_str_value("timezone_id").unwrap_or_default();
|
||||||
let field_type = data
|
|
||||||
.get_i64_value("field_type")
|
|
||||||
.map(FieldType::from)
|
|
||||||
.unwrap_or(FieldType::DateTime);
|
|
||||||
Self {
|
Self {
|
||||||
date_format,
|
date_format,
|
||||||
time_format,
|
time_format,
|
||||||
timezone_id,
|
timezone_id,
|
||||||
field_type,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -78,7 +71,6 @@ impl From<DateTypeOption> for TypeOptionData {
|
|||||||
.insert_i64_value("date_format", data.date_format.value())
|
.insert_i64_value("date_format", data.date_format.value())
|
||||||
.insert_i64_value("time_format", data.time_format.value())
|
.insert_i64_value("time_format", data.time_format.value())
|
||||||
.insert_str_value("timezone_id", data.timezone_id)
|
.insert_str_value("timezone_id", data.timezone_id)
|
||||||
.insert_i64_value("field_type", data.field_type.value())
|
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -88,7 +80,16 @@ 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 {
|
||||||
self.today_desc_from_timestamp(cell_data)
|
let timestamp = cell_data.timestamp;
|
||||||
|
let include_time = cell_data.include_time;
|
||||||
|
let (date, time) = self.formatted_date_time_from_timestamp(×tamp);
|
||||||
|
|
||||||
|
DateCellDataPB {
|
||||||
|
date,
|
||||||
|
time,
|
||||||
|
timestamp: timestamp.unwrap_or_default(),
|
||||||
|
include_time,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_cell(&self, cell: &Cell) -> FlowyResult<<Self as TypeOption>::CellData> {
|
fn parse_cell(&self, cell: &Cell) -> FlowyResult<<Self as TypeOption>::CellData> {
|
||||||
@ -97,28 +98,20 @@ impl TypeOptionCellDataSerde for DateTypeOption {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl DateTypeOption {
|
impl DateTypeOption {
|
||||||
pub fn new(field_type: FieldType) -> Self {
|
pub fn new() -> Self {
|
||||||
Self {
|
Self::default()
|
||||||
field_type,
|
|
||||||
..Default::default()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn test() -> Self {
|
pub fn test() -> Self {
|
||||||
Self {
|
Self {
|
||||||
timezone_id: "Etc/UTC".to_owned(),
|
timezone_id: "Etc/UTC".to_owned(),
|
||||||
field_type: FieldType::DateTime,
|
|
||||||
..Self::default()
|
..Self::default()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn today_desc_from_timestamp(&self, cell_data: DateCellData) -> DateCellDataPB {
|
fn formatted_date_time_from_timestamp(&self, timestamp: &Option<i64>) -> (String, String) {
|
||||||
let timestamp = cell_data.timestamp.unwrap_or_default();
|
if let Some(timestamp) = timestamp {
|
||||||
let include_time = cell_data.include_time;
|
let naive = chrono::NaiveDateTime::from_timestamp_opt(*timestamp, 0).unwrap();
|
||||||
|
|
||||||
let (date, time) = match cell_data.timestamp {
|
|
||||||
Some(timestamp) => {
|
|
||||||
let naive = chrono::NaiveDateTime::from_timestamp_opt(timestamp, 0).unwrap();
|
|
||||||
let offset = self.get_timezone_offset(naive);
|
let offset = self.get_timezone_offset(naive);
|
||||||
let date_time = DateTime::<Local>::from_utc(naive, offset);
|
let date_time = DateTime::<Local>::from_utc(naive, offset);
|
||||||
|
|
||||||
@ -126,17 +119,29 @@ impl DateTypeOption {
|
|||||||
let date = format!("{}", date_time.format(fmt));
|
let date = format!("{}", date_time.format(fmt));
|
||||||
let fmt = self.time_format.format_str();
|
let fmt = self.time_format.format_str();
|
||||||
let time = format!("{}", date_time.format(fmt));
|
let time = format!("{}", date_time.format(fmt));
|
||||||
|
|
||||||
(date, time)
|
(date, time)
|
||||||
},
|
} else {
|
||||||
None => ("".to_owned(), "".to_owned()),
|
("".to_owned(), "".to_owned())
|
||||||
};
|
}
|
||||||
|
}
|
||||||
|
|
||||||
DateCellDataPB {
|
fn naive_time_from_time_string(
|
||||||
date,
|
&self,
|
||||||
time,
|
include_time: bool,
|
||||||
include_time,
|
time_str: Option<String>,
|
||||||
timestamp,
|
) -> FlowyResult<Option<NaiveTime>> {
|
||||||
|
match (include_time, time_str) {
|
||||||
|
(true, Some(time_str)) => {
|
||||||
|
let result = NaiveTime::parse_from_str(&time_str, self.time_format.format_str());
|
||||||
|
match result {
|
||||||
|
Ok(time) => Ok(Some(time)),
|
||||||
|
Err(_e) => {
|
||||||
|
let msg = format!("Parse {} failed", time_str);
|
||||||
|
Err(FlowyError::new(ErrorCode::InvalidDateTimeFormat, msg))
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
_ => Ok(None),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -211,7 +216,14 @@ 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 {
|
||||||
self.today_desc_from_timestamp(cell_data).date
|
let timestamp = cell_data.timestamp;
|
||||||
|
let include_time = cell_data.include_time;
|
||||||
|
let (date_string, time_string) = self.formatted_date_time_from_timestamp(×tamp);
|
||||||
|
if include_time && timestamp.is_some() {
|
||||||
|
format!("{} {}", date_string, time_string)
|
||||||
|
} else {
|
||||||
|
date_string
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn stringify_cell(&self, cell: &Cell) -> String {
|
fn stringify_cell(&self, cell: &Cell) -> String {
|
||||||
@ -236,15 +248,12 @@ impl CellDataChangeset for DateTypeOption {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if changeset.clear_flag == Some(true) {
|
if changeset.clear_flag == Some(true) {
|
||||||
let (timestamp, include_time) = (None, include_time);
|
|
||||||
|
|
||||||
let cell_data = DateCellData {
|
let cell_data = DateCellData {
|
||||||
timestamp,
|
timestamp: None,
|
||||||
include_time,
|
include_time,
|
||||||
};
|
};
|
||||||
|
|
||||||
let cell_wrapper: DateCellDataWrapper = (self.field_type.clone(), cell_data.clone()).into();
|
return Ok((Cell::from(&cell_data), cell_data));
|
||||||
return Ok((Cell::from(cell_wrapper), cell_data));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// update include_time if necessary
|
// update include_time if necessary
|
||||||
@ -256,27 +265,12 @@ 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 changeset_timestamp = changeset.date;
|
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_time = match (include_time, changeset.time) {
|
|
||||||
(true, Some(time_str)) => {
|
|
||||||
let result = NaiveTime::parse_from_str(&time_str, self.time_format.format_str());
|
|
||||||
match result {
|
|
||||||
Ok(time) => Ok(Some(time)),
|
|
||||||
Err(_e) => {
|
|
||||||
let msg = format!("Parse {} failed", time_str);
|
|
||||||
Err(FlowyError::new(ErrorCode::InvalidDateTimeFormat, msg))
|
|
||||||
},
|
|
||||||
}
|
|
||||||
},
|
|
||||||
_ => Ok(None),
|
|
||||||
}?;
|
|
||||||
|
|
||||||
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_time,
|
||||||
previous_timestamp,
|
previous_timestamp,
|
||||||
changeset_timestamp,
|
changeset.date,
|
||||||
);
|
);
|
||||||
|
|
||||||
let cell_data = DateCellData {
|
let cell_data = DateCellData {
|
||||||
@ -284,8 +278,7 @@ impl CellDataChangeset for DateTypeOption {
|
|||||||
include_time,
|
include_time,
|
||||||
};
|
};
|
||||||
|
|
||||||
let cell_wrapper: DateCellDataWrapper = (self.field_type.clone(), cell_data.clone()).into();
|
Ok((Cell::from(&cell_data), cell_data))
|
||||||
Ok((Cell::from(cell_wrapper), cell_data))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -83,42 +83,19 @@ impl From<&DateCellDataPB> for DateCellData {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Wrapper for DateCellData that also contains the field type.
|
impl From<&DateCellData> for Cell {
|
||||||
/// Handy struct to use when you need to convert a DateCellData to a Cell.
|
fn from(cell_data: &DateCellData) -> Self {
|
||||||
pub struct DateCellDataWrapper {
|
let timestamp_string = match cell_data.timestamp {
|
||||||
data: DateCellData,
|
|
||||||
field_type: FieldType,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<(FieldType, DateCellData)> for DateCellDataWrapper {
|
|
||||||
fn from((field_type, data): (FieldType, DateCellData)) -> Self {
|
|
||||||
Self { data, field_type }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<DateCellDataWrapper> for Cell {
|
|
||||||
fn from(wrapper: DateCellDataWrapper) -> Self {
|
|
||||||
let (field_type, data) = (wrapper.field_type, wrapper.data);
|
|
||||||
let timestamp_string = match data.timestamp {
|
|
||||||
Some(timestamp) => timestamp.to_string(),
|
Some(timestamp) => timestamp.to_string(),
|
||||||
None => "".to_owned(),
|
None => "".to_owned(),
|
||||||
};
|
};
|
||||||
// Most of the case, don't use these keys in other places. Otherwise, we should define
|
new_cell_builder(FieldType::DateTime)
|
||||||
// constants for them.
|
|
||||||
new_cell_builder(field_type)
|
|
||||||
.insert_str_value(CELL_DATA, timestamp_string)
|
.insert_str_value(CELL_DATA, timestamp_string)
|
||||||
.insert_bool_value("include_time", data.include_time)
|
.insert_bool_value("include_time", cell_data.include_time)
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<DateCellData> for Cell {
|
|
||||||
fn from(data: DateCellData) -> Self {
|
|
||||||
let data: DateCellDataWrapper = (FieldType::DateTime, data).into();
|
|
||||||
Cell::from(data)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'de> serde::Deserialize<'de> for DateCellData {
|
impl<'de> serde::Deserialize<'de> for DateCellData {
|
||||||
fn deserialize<D>(deserializer: D) -> core::result::Result<Self, D::Error>
|
fn deserialize<D>(deserializer: D) -> core::result::Result<Self, D::Error>
|
||||||
where
|
where
|
||||||
|
@ -4,6 +4,7 @@ pub mod date_type_option;
|
|||||||
pub mod number_type_option;
|
pub mod number_type_option;
|
||||||
pub mod selection_type_option;
|
pub mod selection_type_option;
|
||||||
pub mod text_type_option;
|
pub mod text_type_option;
|
||||||
|
pub mod timestamp_type_option;
|
||||||
mod type_option;
|
mod type_option;
|
||||||
mod type_option_cell;
|
mod type_option_cell;
|
||||||
mod url_type_option;
|
mod url_type_option;
|
||||||
@ -14,6 +15,7 @@ pub use date_type_option::*;
|
|||||||
pub use number_type_option::*;
|
pub use number_type_option::*;
|
||||||
pub use selection_type_option::*;
|
pub use selection_type_option::*;
|
||||||
pub use text_type_option::*;
|
pub use text_type_option::*;
|
||||||
|
pub use timestamp_type_option::*;
|
||||||
pub use type_option::*;
|
pub use type_option::*;
|
||||||
pub use type_option_cell::*;
|
pub use type_option_cell::*;
|
||||||
pub use url_type_option::*;
|
pub use url_type_option::*;
|
||||||
|
@ -30,8 +30,8 @@ mod tests {
|
|||||||
};
|
};
|
||||||
|
|
||||||
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"
|
"Mar 14, 2022 09:56"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -238,14 +238,14 @@ impl TypeOptionCellData for StrCellData {
|
|||||||
|
|
||||||
impl From<&Cell> for StrCellData {
|
impl From<&Cell> for StrCellData {
|
||||||
fn from(cell: &Cell) -> Self {
|
fn from(cell: &Cell) -> Self {
|
||||||
Self(cell.get_str_value("data").unwrap_or_default())
|
Self(cell.get_str_value(CELL_DATA).unwrap_or_default())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<StrCellData> for Cell {
|
impl From<StrCellData> for Cell {
|
||||||
fn from(data: StrCellData) -> Self {
|
fn from(data: StrCellData) -> Self {
|
||||||
new_cell_builder(FieldType::RichText)
|
new_cell_builder(FieldType::RichText)
|
||||||
.insert_str_value("data", data.0)
|
.insert_str_value(CELL_DATA, data.0)
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,6 @@
|
|||||||
|
#![allow(clippy::module_inception)]
|
||||||
|
mod timestamp_type_option;
|
||||||
|
mod timestamp_type_option_entities;
|
||||||
|
|
||||||
|
pub use timestamp_type_option::*;
|
||||||
|
pub use timestamp_type_option_entities::*;
|
@ -0,0 +1,205 @@
|
|||||||
|
use std::cmp::Ordering;
|
||||||
|
|
||||||
|
use chrono::{DateTime, Local, Offset};
|
||||||
|
use collab::core::any_map::AnyMapExtension;
|
||||||
|
use collab_database::fields::{Field, TypeOptionData, TypeOptionDataBuilder};
|
||||||
|
use collab_database::rows::Cell;
|
||||||
|
use flowy_error::{ErrorCode, FlowyError, FlowyResult};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use crate::entities::{DateFilterPB, FieldType, TimestampCellDataPB};
|
||||||
|
use crate::services::cell::{CellDataChangeset, CellDataDecoder};
|
||||||
|
use crate::services::field::{
|
||||||
|
default_order, DateFormat, TimeFormat, TimestampCellData, TypeOption, TypeOptionCellDataCompare,
|
||||||
|
TypeOptionCellDataFilter, TypeOptionCellDataSerde, TypeOptionTransform,
|
||||||
|
};
|
||||||
|
use crate::services::sort::SortCondition;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
pub struct TimestampTypeOption {
|
||||||
|
pub date_format: DateFormat,
|
||||||
|
pub time_format: TimeFormat,
|
||||||
|
pub include_time: bool,
|
||||||
|
pub field_type: FieldType,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for TimestampTypeOption {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
date_format: Default::default(),
|
||||||
|
time_format: Default::default(),
|
||||||
|
include_time: true,
|
||||||
|
field_type: FieldType::LastEditedTime,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TypeOption for TimestampTypeOption {
|
||||||
|
type CellData = TimestampCellData;
|
||||||
|
type CellChangeset = String;
|
||||||
|
type CellProtobufType = TimestampCellDataPB;
|
||||||
|
type CellFilter = DateFilterPB;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<TypeOptionData> for TimestampTypeOption {
|
||||||
|
fn from(data: TypeOptionData) -> Self {
|
||||||
|
let date_format = data
|
||||||
|
.get_i64_value("date_format")
|
||||||
|
.map(DateFormat::from)
|
||||||
|
.unwrap_or_default();
|
||||||
|
let time_format = data
|
||||||
|
.get_i64_value("time_format")
|
||||||
|
.map(TimeFormat::from)
|
||||||
|
.unwrap_or_default();
|
||||||
|
let include_time = data.get_bool_value("include_time").unwrap_or_default();
|
||||||
|
let field_type = data
|
||||||
|
.get_i64_value("field_type")
|
||||||
|
.map(FieldType::from)
|
||||||
|
.unwrap_or(FieldType::LastEditedTime);
|
||||||
|
Self {
|
||||||
|
date_format,
|
||||||
|
time_format,
|
||||||
|
include_time,
|
||||||
|
field_type,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<TimestampTypeOption> for TypeOptionData {
|
||||||
|
fn from(option: TimestampTypeOption) -> Self {
|
||||||
|
TypeOptionDataBuilder::new()
|
||||||
|
.insert_i64_value("date_format", option.date_format.value())
|
||||||
|
.insert_i64_value("time_format", option.time_format.value())
|
||||||
|
.insert_bool_value("include_time", option.include_time)
|
||||||
|
.insert_i64_value("field_type", option.field_type.value())
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TypeOptionCellDataSerde for TimestampTypeOption {
|
||||||
|
fn protobuf_encode(
|
||||||
|
&self,
|
||||||
|
cell_data: <Self as TypeOption>::CellData,
|
||||||
|
) -> <Self as TypeOption>::CellProtobufType {
|
||||||
|
let timestamp = cell_data.timestamp;
|
||||||
|
let date_time = self.stringify_cell_data(cell_data);
|
||||||
|
|
||||||
|
TimestampCellDataPB {
|
||||||
|
date_time,
|
||||||
|
timestamp,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_cell(&self, cell: &Cell) -> FlowyResult<<Self as TypeOption>::CellData> {
|
||||||
|
Ok(TimestampCellData::from(cell))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TimestampTypeOption {
|
||||||
|
pub fn new(field_type: FieldType) -> Self {
|
||||||
|
Self {
|
||||||
|
field_type,
|
||||||
|
include_time: true,
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn formatted_date_time_from_timestamp(&self, timestamp: &Option<i64>) -> (String, String) {
|
||||||
|
if let Some(timestamp) = timestamp {
|
||||||
|
let naive = chrono::NaiveDateTime::from_timestamp_opt(*timestamp, 0).unwrap();
|
||||||
|
let offset = Local::now().offset().fix();
|
||||||
|
let date_time = DateTime::<Local>::from_utc(naive, offset);
|
||||||
|
|
||||||
|
let fmt = self.date_format.format_str();
|
||||||
|
let date = format!("{}", date_time.format(fmt));
|
||||||
|
let fmt = self.time_format.format_str();
|
||||||
|
let time = format!("{}", date_time.format(fmt));
|
||||||
|
(date, time)
|
||||||
|
} else {
|
||||||
|
("".to_owned(), "".to_owned())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TypeOptionTransform for TimestampTypeOption {}
|
||||||
|
|
||||||
|
impl CellDataDecoder for TimestampTypeOption {
|
||||||
|
fn decode_cell(
|
||||||
|
&self,
|
||||||
|
cell: &Cell,
|
||||||
|
decoded_field_type: &FieldType,
|
||||||
|
_field: &Field,
|
||||||
|
) -> FlowyResult<<Self as TypeOption>::CellData> {
|
||||||
|
// Return default data if the type_option_cell_data is not FieldType::DateTime.
|
||||||
|
// It happens when switching from one field to another.
|
||||||
|
// For example:
|
||||||
|
// FieldType::RichText -> FieldType::DateTime, it will display empty content on the screen.
|
||||||
|
if !decoded_field_type.is_date() {
|
||||||
|
return Ok(Default::default());
|
||||||
|
}
|
||||||
|
|
||||||
|
self.parse_cell(cell)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn stringify_cell_data(&self, cell_data: <Self as TypeOption>::CellData) -> String {
|
||||||
|
let timestamp = cell_data.timestamp;
|
||||||
|
let (date_string, time_string) = self.formatted_date_time_from_timestamp(×tamp);
|
||||||
|
if self.include_time {
|
||||||
|
format!("{} {}", date_string, time_string)
|
||||||
|
} else {
|
||||||
|
date_string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn stringify_cell(&self, cell: &Cell) -> String {
|
||||||
|
let cell_data = Self::CellData::from(cell);
|
||||||
|
self.stringify_cell_data(cell_data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CellDataChangeset for TimestampTypeOption {
|
||||||
|
fn apply_changeset(
|
||||||
|
&self,
|
||||||
|
_changeset: <Self as TypeOption>::CellChangeset,
|
||||||
|
_cell: Option<Cell>,
|
||||||
|
) -> FlowyResult<(Cell, <Self as TypeOption>::CellData)> {
|
||||||
|
Err(FlowyError::new(
|
||||||
|
ErrorCode::FieldInvalidOperation,
|
||||||
|
"Cells of this field type cannot be edited",
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TypeOptionCellDataFilter for TimestampTypeOption {
|
||||||
|
fn apply_filter(
|
||||||
|
&self,
|
||||||
|
filter: &<Self as TypeOption>::CellFilter,
|
||||||
|
field_type: &FieldType,
|
||||||
|
cell_data: &<Self as TypeOption>::CellData,
|
||||||
|
) -> bool {
|
||||||
|
if !field_type.is_date() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
filter.is_visible(cell_data.timestamp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TypeOptionCellDataCompare for TimestampTypeOption {
|
||||||
|
fn apply_cmp(
|
||||||
|
&self,
|
||||||
|
cell_data: &<Self as TypeOption>::CellData,
|
||||||
|
other_cell_data: &<Self as TypeOption>::CellData,
|
||||||
|
sort_condition: SortCondition,
|
||||||
|
) -> Ordering {
|
||||||
|
match (cell_data.timestamp, other_cell_data.timestamp) {
|
||||||
|
(Some(left), Some(right)) => {
|
||||||
|
let order = left.cmp(&right);
|
||||||
|
sort_condition.evaluate_order(order)
|
||||||
|
},
|
||||||
|
(Some(_), None) => Ordering::Less,
|
||||||
|
(None, Some(_)) => Ordering::Greater,
|
||||||
|
(None, None) => default_order(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,69 @@
|
|||||||
|
use collab::core::any_map::AnyMapExtension;
|
||||||
|
use collab_database::rows::{new_cell_builder, Cell};
|
||||||
|
use serde::Serialize;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
entities::FieldType,
|
||||||
|
services::field::{TypeOptionCellData, CELL_DATA},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Default, Serialize)]
|
||||||
|
pub struct TimestampCellData {
|
||||||
|
pub timestamp: Option<i64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TimestampCellData {
|
||||||
|
pub fn new(timestamp: i64) -> Self {
|
||||||
|
Self {
|
||||||
|
timestamp: Some(timestamp),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&Cell> for TimestampCellData {
|
||||||
|
fn from(cell: &Cell) -> Self {
|
||||||
|
let timestamp = cell
|
||||||
|
.get_str_value(CELL_DATA)
|
||||||
|
.and_then(|data| data.parse::<i64>().ok());
|
||||||
|
Self { timestamp }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Wrapper for DateCellData that also contains the field type.
|
||||||
|
/// Handy struct to use when you need to convert a DateCellData to a Cell.
|
||||||
|
pub struct TimestampCellDataWrapper {
|
||||||
|
data: TimestampCellData,
|
||||||
|
field_type: FieldType,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<(FieldType, TimestampCellData)> for TimestampCellDataWrapper {
|
||||||
|
fn from((field_type, data): (FieldType, TimestampCellData)) -> Self {
|
||||||
|
Self { data, field_type }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<TimestampCellDataWrapper> for Cell {
|
||||||
|
fn from(wrapper: TimestampCellDataWrapper) -> Self {
|
||||||
|
let (field_type, data) = (wrapper.field_type, wrapper.data);
|
||||||
|
let timestamp_string = data.timestamp.unwrap_or_default();
|
||||||
|
|
||||||
|
new_cell_builder(field_type)
|
||||||
|
.insert_str_value(CELL_DATA, timestamp_string)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<TimestampCellData> for Cell {
|
||||||
|
fn from(data: TimestampCellData) -> Self {
|
||||||
|
let data: TimestampCellDataWrapper = (FieldType::LastEditedTime, data).into();
|
||||||
|
Cell::from(data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TypeOptionCellData for TimestampCellData {}
|
||||||
|
|
||||||
|
impl ToString for TimestampCellData {
|
||||||
|
fn to_string(&self) -> String {
|
||||||
|
serde_json::to_string(self).unwrap()
|
||||||
|
}
|
||||||
|
}
|
@ -11,13 +11,13 @@ use flowy_error::FlowyResult;
|
|||||||
use crate::entities::{
|
use crate::entities::{
|
||||||
CheckboxTypeOptionPB, ChecklistTypeOptionPB, DateTypeOptionPB, FieldType,
|
CheckboxTypeOptionPB, ChecklistTypeOptionPB, DateTypeOptionPB, FieldType,
|
||||||
MultiSelectTypeOptionPB, NumberTypeOptionPB, RichTextTypeOptionPB, SingleSelectTypeOptionPB,
|
MultiSelectTypeOptionPB, NumberTypeOptionPB, RichTextTypeOptionPB, SingleSelectTypeOptionPB,
|
||||||
URLTypeOptionPB,
|
TimestampTypeOptionPB, URLTypeOptionPB,
|
||||||
};
|
};
|
||||||
use crate::services::cell::{CellDataDecoder, FromCellChangeset, ToCellChangeset};
|
use crate::services::cell::{CellDataDecoder, FromCellChangeset, ToCellChangeset};
|
||||||
use crate::services::field::checklist_type_option::ChecklistTypeOption;
|
use crate::services::field::checklist_type_option::ChecklistTypeOption;
|
||||||
use crate::services::field::{
|
use crate::services::field::{
|
||||||
CheckboxTypeOption, DateFormat, DateTypeOption, MultiSelectTypeOption, NumberTypeOption,
|
CheckboxTypeOption, DateFormat, DateTypeOption, MultiSelectTypeOption, NumberTypeOption,
|
||||||
RichTextTypeOption, SingleSelectTypeOption, TimeFormat, URLTypeOption,
|
RichTextTypeOption, SingleSelectTypeOption, TimeFormat, TimestampTypeOption, URLTypeOption,
|
||||||
};
|
};
|
||||||
use crate::services::filter::FromFilterString;
|
use crate::services::filter::FromFilterString;
|
||||||
use crate::services::sort::SortCondition;
|
use crate::services::sort::SortCondition;
|
||||||
@ -179,9 +179,12 @@ pub fn type_option_data_from_pb_or_default<T: Into<Bytes>>(
|
|||||||
FieldType::Number => {
|
FieldType::Number => {
|
||||||
NumberTypeOptionPB::try_from(bytes).map(|pb| NumberTypeOption::from(pb).into())
|
NumberTypeOptionPB::try_from(bytes).map(|pb| NumberTypeOption::from(pb).into())
|
||||||
},
|
},
|
||||||
FieldType::DateTime | FieldType::LastEditedTime | FieldType::CreatedTime => {
|
FieldType::DateTime => {
|
||||||
DateTypeOptionPB::try_from(bytes).map(|pb| DateTypeOption::from(pb).into())
|
DateTypeOptionPB::try_from(bytes).map(|pb| DateTypeOption::from(pb).into())
|
||||||
},
|
},
|
||||||
|
FieldType::LastEditedTime | FieldType::CreatedTime => {
|
||||||
|
TimestampTypeOptionPB::try_from(bytes).map(|pb| TimestampTypeOption::from(pb).into())
|
||||||
|
},
|
||||||
FieldType::SingleSelect => {
|
FieldType::SingleSelect => {
|
||||||
SingleSelectTypeOptionPB::try_from(bytes).map(|pb| SingleSelectTypeOption::from(pb).into())
|
SingleSelectTypeOptionPB::try_from(bytes).map(|pb| SingleSelectTypeOption::from(pb).into())
|
||||||
},
|
},
|
||||||
@ -214,10 +217,16 @@ pub fn type_option_to_pb(type_option: TypeOptionData, field_type: &FieldType) ->
|
|||||||
.try_into()
|
.try_into()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
},
|
},
|
||||||
FieldType::DateTime | FieldType::LastEditedTime | FieldType::CreatedTime => {
|
FieldType::DateTime => {
|
||||||
let date_type_option: DateTypeOption = type_option.into();
|
let date_type_option: DateTypeOption = type_option.into();
|
||||||
DateTypeOptionPB::from(date_type_option).try_into().unwrap()
|
DateTypeOptionPB::from(date_type_option).try_into().unwrap()
|
||||||
},
|
},
|
||||||
|
FieldType::LastEditedTime | FieldType::CreatedTime => {
|
||||||
|
let timestamp_type_option: TimestampTypeOption = type_option.into();
|
||||||
|
TimestampTypeOptionPB::from(timestamp_type_option)
|
||||||
|
.try_into()
|
||||||
|
.unwrap()
|
||||||
|
},
|
||||||
FieldType::SingleSelect => {
|
FieldType::SingleSelect => {
|
||||||
let single_select_type_option: SingleSelectTypeOption = type_option.into();
|
let single_select_type_option: SingleSelectTypeOption = type_option.into();
|
||||||
SingleSelectTypeOptionPB::from(single_select_type_option)
|
SingleSelectTypeOptionPB::from(single_select_type_option)
|
||||||
@ -254,14 +263,14 @@ pub fn default_type_option_data_from_type(field_type: &FieldType) -> TypeOptionD
|
|||||||
FieldType::RichText => RichTextTypeOption::default().into(),
|
FieldType::RichText => RichTextTypeOption::default().into(),
|
||||||
FieldType::Number => NumberTypeOption::default().into(),
|
FieldType::Number => NumberTypeOption::default().into(),
|
||||||
FieldType::DateTime => DateTypeOption {
|
FieldType::DateTime => DateTypeOption {
|
||||||
field_type: field_type.clone(),
|
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}
|
}
|
||||||
.into(),
|
.into(),
|
||||||
FieldType::LastEditedTime | FieldType::CreatedTime => DateTypeOption {
|
FieldType::LastEditedTime | FieldType::CreatedTime => TimestampTypeOption {
|
||||||
field_type: field_type.clone(),
|
field_type: field_type.clone(),
|
||||||
date_format: DateFormat::Friendly,
|
date_format: DateFormat::Friendly,
|
||||||
time_format: TimeFormat::TwelveHour,
|
time_format: TimeFormat::TwelveHour,
|
||||||
|
include_time: true,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}
|
}
|
||||||
.into(),
|
.into(),
|
||||||
|
@ -16,8 +16,8 @@ use crate::services::cell::{
|
|||||||
use crate::services::field::checklist_type_option::ChecklistTypeOption;
|
use crate::services::field::checklist_type_option::ChecklistTypeOption;
|
||||||
use crate::services::field::{
|
use crate::services::field::{
|
||||||
CheckboxTypeOption, DateTypeOption, MultiSelectTypeOption, NumberTypeOption, RichTextTypeOption,
|
CheckboxTypeOption, DateTypeOption, MultiSelectTypeOption, NumberTypeOption, RichTextTypeOption,
|
||||||
SingleSelectTypeOption, TypeOption, TypeOptionCellDataCompare, TypeOptionCellDataFilter,
|
SingleSelectTypeOption, TimestampTypeOption, TypeOption, TypeOptionCellDataCompare,
|
||||||
TypeOptionCellDataSerde, TypeOptionTransform, URLTypeOption,
|
TypeOptionCellDataFilter, TypeOptionCellDataSerde, TypeOptionTransform, URLTypeOption,
|
||||||
};
|
};
|
||||||
use crate::services::sort::SortCondition;
|
use crate::services::sort::SortCondition;
|
||||||
|
|
||||||
@ -407,7 +407,7 @@ impl<'a> TypeOptionCellExt<'a> {
|
|||||||
self.cell_data_cache.clone(),
|
self.cell_data_cache.clone(),
|
||||||
)
|
)
|
||||||
}),
|
}),
|
||||||
FieldType::DateTime | FieldType::LastEditedTime | FieldType::CreatedTime => self
|
FieldType::DateTime => self
|
||||||
.field
|
.field
|
||||||
.get_type_option::<DateTypeOption>(field_type)
|
.get_type_option::<DateTypeOption>(field_type)
|
||||||
.map(|type_option| {
|
.map(|type_option| {
|
||||||
@ -417,6 +417,16 @@ impl<'a> TypeOptionCellExt<'a> {
|
|||||||
self.cell_data_cache.clone(),
|
self.cell_data_cache.clone(),
|
||||||
)
|
)
|
||||||
}),
|
}),
|
||||||
|
FieldType::LastEditedTime | FieldType::CreatedTime => self
|
||||||
|
.field
|
||||||
|
.get_type_option::<TimestampTypeOption>(field_type)
|
||||||
|
.map(|type_option| {
|
||||||
|
TypeOptionCellDataHandlerImpl::new_with_boxed(
|
||||||
|
type_option,
|
||||||
|
self.cell_filter_cache.clone(),
|
||||||
|
self.cell_data_cache.clone(),
|
||||||
|
)
|
||||||
|
}),
|
||||||
FieldType::SingleSelect => self
|
FieldType::SingleSelect => self
|
||||||
.field
|
.field
|
||||||
.get_type_option::<SingleSelectTypeOption>(field_type)
|
.get_type_option::<SingleSelectTypeOption>(field_type)
|
||||||
@ -527,9 +537,12 @@ fn get_type_option_transform_handler(
|
|||||||
FieldType::Number => {
|
FieldType::Number => {
|
||||||
Box::new(NumberTypeOption::from(type_option_data)) as Box<dyn TypeOptionTransformHandler>
|
Box::new(NumberTypeOption::from(type_option_data)) as Box<dyn TypeOptionTransformHandler>
|
||||||
},
|
},
|
||||||
FieldType::DateTime | FieldType::LastEditedTime | FieldType::CreatedTime => {
|
FieldType::DateTime => {
|
||||||
Box::new(DateTypeOption::from(type_option_data)) as Box<dyn TypeOptionTransformHandler>
|
Box::new(DateTypeOption::from(type_option_data)) as Box<dyn TypeOptionTransformHandler>
|
||||||
},
|
},
|
||||||
|
FieldType::LastEditedTime | FieldType::CreatedTime => {
|
||||||
|
Box::new(TimestampTypeOption::from(type_option_data)) as Box<dyn TypeOptionTransformHandler>
|
||||||
|
},
|
||||||
FieldType::SingleSelect => Box::new(SingleSelectTypeOption::from(type_option_data))
|
FieldType::SingleSelect => Box::new(SingleSelectTypeOption::from(type_option_data))
|
||||||
as Box<dyn TypeOptionTransformHandler>,
|
as Box<dyn TypeOptionTransformHandler>,
|
||||||
FieldType::MultiSelect => {
|
FieldType::MultiSelect => {
|
||||||
@ -590,6 +603,10 @@ impl RowSingleCellData {
|
|||||||
into_date_field_cell_data,
|
into_date_field_cell_data,
|
||||||
<DateTypeOption as TypeOption>::CellData
|
<DateTypeOption as TypeOption>::CellData
|
||||||
);
|
);
|
||||||
|
into_cell_data!(
|
||||||
|
into_timestamp_field_cell_data,
|
||||||
|
<TimestampTypeOption as TypeOption>::CellData
|
||||||
|
);
|
||||||
into_cell_data!(
|
into_cell_data!(
|
||||||
into_check_list_field_cell_data,
|
into_check_list_field_cell_data,
|
||||||
<CheckboxTypeOption as TypeOption>::CellData
|
<CheckboxTypeOption as TypeOption>::CellData
|
||||||
|
@ -455,7 +455,6 @@ mod tests {
|
|||||||
|
|
||||||
use chrono::{offset, Days, Duration, NaiveDateTime};
|
use chrono::{offset, Days, Duration, NaiveDateTime};
|
||||||
|
|
||||||
use crate::entities::FieldType;
|
|
||||||
use crate::services::{
|
use crate::services::{
|
||||||
field::{date_type_option::DateTypeOption, DateCellData},
|
field::{date_type_option::DateTypeOption, DateCellData},
|
||||||
group::controller_impls::date_controller::{
|
group::controller_impls::date_controller::{
|
||||||
@ -481,9 +480,9 @@ mod tests {
|
|||||||
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();
|
||||||
|
|
||||||
let mut local_date_type_option = DateTypeOption::new(FieldType::DateTime);
|
let mut local_date_type_option = DateTypeOption::new();
|
||||||
local_date_type_option.timezone_id = today.offset().to_string();
|
local_date_type_option.timezone_id = today.offset().to_string();
|
||||||
let mut default_date_type_option = DateTypeOption::new(FieldType::DateTime);
|
let mut default_date_type_option = DateTypeOption::new();
|
||||||
default_date_type_option.timezone_id = "".to_string();
|
default_date_type_option.timezone_id = "".to_string();
|
||||||
|
|
||||||
let tests = vec![
|
let tests = vec![
|
||||||
|
@ -22,12 +22,13 @@ async fn grid_cell_update() {
|
|||||||
for (_, row_detail) in rows.iter().enumerate() {
|
for (_, row_detail) in rows.iter().enumerate() {
|
||||||
for field in &fields {
|
for field in &fields {
|
||||||
let field_type = FieldType::from(field.field_type);
|
let field_type = FieldType::from(field.field_type);
|
||||||
|
if field_type == FieldType::LastEditedTime || field_type == FieldType::CreatedTime {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
let cell_changeset = match field_type {
|
let cell_changeset = match field_type {
|
||||||
FieldType::RichText => "".to_string(),
|
FieldType::RichText => "".to_string(),
|
||||||
FieldType::Number => "123".to_string(),
|
FieldType::Number => "123".to_string(),
|
||||||
FieldType::DateTime | FieldType::LastEditedTime | FieldType::CreatedTime => {
|
FieldType::DateTime => make_date_cell_string(123),
|
||||||
make_date_cell_string(123)
|
|
||||||
},
|
|
||||||
FieldType::SingleSelect => {
|
FieldType::SingleSelect => {
|
||||||
let type_option = field
|
let type_option = field
|
||||||
.get_type_option::<SingleSelectTypeOption>(field.field_type)
|
.get_type_option::<SingleSelectTypeOption>(field.field_type)
|
||||||
@ -49,6 +50,7 @@ async fn grid_cell_update() {
|
|||||||
.to_cell_changeset_str(),
|
.to_cell_changeset_str(),
|
||||||
FieldType::Checkbox => "1".to_string(),
|
FieldType::Checkbox => "1".to_string(),
|
||||||
FieldType::URL => "1".to_string(),
|
FieldType::URL => "1".to_string(),
|
||||||
|
_ => "".to_string(),
|
||||||
};
|
};
|
||||||
|
|
||||||
scripts.push(UpdateCell {
|
scripts.push(UpdateCell {
|
||||||
|
@ -31,7 +31,7 @@ async fn grid_create_field() {
|
|||||||
];
|
];
|
||||||
test.run_scripts(scripts).await;
|
test.run_scripts(scripts).await;
|
||||||
|
|
||||||
let (params, field) = create_date_field(&test.view_id(), FieldType::CreatedTime);
|
let (params, field) = create_timestamp_field(&test.view_id(), FieldType::CreatedTime);
|
||||||
let scripts = vec![
|
let scripts = vec![
|
||||||
CreateField { params },
|
CreateField { params },
|
||||||
AssertFieldTypeOptionEqual {
|
AssertFieldTypeOptionEqual {
|
||||||
|
@ -2,7 +2,7 @@ use collab_database::fields::Field;
|
|||||||
use flowy_database2::entities::{CreateFieldParams, FieldType};
|
use flowy_database2::entities::{CreateFieldParams, FieldType};
|
||||||
use flowy_database2::services::field::{
|
use flowy_database2::services::field::{
|
||||||
type_option_to_pb, DateCellChangeset, DateFormat, DateTypeOption, FieldBuilder,
|
type_option_to_pb, DateCellChangeset, DateFormat, DateTypeOption, FieldBuilder,
|
||||||
RichTextTypeOption, SelectOption, SingleSelectTypeOption, TimeFormat,
|
RichTextTypeOption, SelectOption, SingleSelectTypeOption, TimeFormat, TimestampTypeOption,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn create_text_field(grid_id: &str) -> (CreateFieldParams, Field) {
|
pub fn create_text_field(grid_id: &str) -> (CreateFieldParams, Field) {
|
||||||
@ -41,32 +41,52 @@ pub fn create_single_select_field(grid_id: &str) -> (CreateFieldParams, Field) {
|
|||||||
};
|
};
|
||||||
(params, single_select_field)
|
(params, single_select_field)
|
||||||
}
|
}
|
||||||
|
#[allow(dead_code)]
|
||||||
pub fn create_date_field(grid_id: &str, field_type: FieldType) -> (CreateFieldParams, Field) {
|
pub fn create_date_field(grid_id: &str) -> (CreateFieldParams, Field) {
|
||||||
let date_type_option = DateTypeOption {
|
let date_type_option = DateTypeOption {
|
||||||
date_format: DateFormat::US,
|
date_format: DateFormat::US,
|
||||||
time_format: TimeFormat::TwentyFourHour,
|
time_format: TimeFormat::TwentyFourHour,
|
||||||
timezone_id: "Etc/UTC".to_owned(),
|
timezone_id: "Etc/UTC".to_owned(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let field = FieldBuilder::new(FieldType::DateTime, date_type_option.clone())
|
||||||
|
.name("Date")
|
||||||
|
.visibility(true)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
let type_option_data = type_option_to_pb(date_type_option.into(), &FieldType::DateTime).to_vec();
|
||||||
|
|
||||||
|
let params = CreateFieldParams {
|
||||||
|
view_id: grid_id.to_owned(),
|
||||||
|
field_type: FieldType::DateTime,
|
||||||
|
type_option_data: Some(type_option_data),
|
||||||
|
};
|
||||||
|
(params, field)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn create_timestamp_field(grid_id: &str, field_type: FieldType) -> (CreateFieldParams, Field) {
|
||||||
|
let timestamp_type_option = TimestampTypeOption {
|
||||||
|
date_format: DateFormat::US,
|
||||||
|
time_format: TimeFormat::TwentyFourHour,
|
||||||
|
include_time: true,
|
||||||
field_type: field_type.clone(),
|
field_type: field_type.clone(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let field: Field = match field_type {
|
let field: Field = match field_type {
|
||||||
FieldType::DateTime => FieldBuilder::new(field_type.clone(), date_type_option.clone())
|
FieldType::LastEditedTime => {
|
||||||
.name("Date")
|
FieldBuilder::new(field_type.clone(), timestamp_type_option.clone())
|
||||||
.visibility(true)
|
|
||||||
.build(),
|
|
||||||
FieldType::LastEditedTime => FieldBuilder::new(field_type.clone(), date_type_option.clone())
|
|
||||||
.name("Updated At")
|
.name("Updated At")
|
||||||
.visibility(true)
|
.visibility(true)
|
||||||
.build(),
|
.build()
|
||||||
FieldType::CreatedTime => FieldBuilder::new(field_type.clone(), date_type_option.clone())
|
},
|
||||||
|
FieldType::CreatedTime => FieldBuilder::new(field_type.clone(), timestamp_type_option.clone())
|
||||||
.name("Created At")
|
.name("Created At")
|
||||||
.visibility(true)
|
.visibility(true)
|
||||||
.build(),
|
.build(),
|
||||||
_ => panic!("Unsupported group field type"),
|
_ => panic!("Unsupported group field type"),
|
||||||
};
|
};
|
||||||
|
|
||||||
let type_option_data = type_option_to_pb(date_type_option.into(), &field_type).to_vec();
|
let type_option_data = type_option_to_pb(timestamp_type_option.into(), &field_type).to_vec();
|
||||||
|
|
||||||
let params = CreateFieldParams {
|
let params = CreateFieldParams {
|
||||||
view_id: grid_id.to_owned(),
|
view_id: grid_id.to_owned(),
|
||||||
|
@ -7,7 +7,7 @@ use flowy_database2::entities::FieldType;
|
|||||||
use flowy_database2::services::field::checklist_type_option::ChecklistTypeOption;
|
use flowy_database2::services::field::checklist_type_option::ChecklistTypeOption;
|
||||||
use flowy_database2::services::field::{
|
use flowy_database2::services::field::{
|
||||||
DateFormat, DateTypeOption, FieldBuilder, MultiSelectTypeOption, SelectOption, SelectOptionColor,
|
DateFormat, DateTypeOption, FieldBuilder, MultiSelectTypeOption, SelectOption, SelectOptionColor,
|
||||||
SingleSelectTypeOption, TimeFormat,
|
SingleSelectTypeOption, TimeFormat, TimestampTypeOption,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::database::database_editor::TestRowBuilder;
|
use crate::database::database_editor::TestRowBuilder;
|
||||||
@ -36,17 +36,30 @@ pub fn make_test_board() -> DatabaseData {
|
|||||||
.build();
|
.build();
|
||||||
fields.push(number_field);
|
fields.push(number_field);
|
||||||
},
|
},
|
||||||
FieldType::DateTime | FieldType::LastEditedTime | FieldType::CreatedTime => {
|
FieldType::DateTime => {
|
||||||
// Date
|
// Date
|
||||||
let date_type_option = DateTypeOption {
|
let date_type_option = DateTypeOption {
|
||||||
date_format: DateFormat::US,
|
date_format: DateFormat::US,
|
||||||
time_format: TimeFormat::TwentyFourHour,
|
time_format: TimeFormat::TwentyFourHour,
|
||||||
timezone_id: "Etc/UTC".to_owned(),
|
timezone_id: "Etc/UTC".to_owned(),
|
||||||
|
};
|
||||||
|
let name = "Time";
|
||||||
|
let date_field = FieldBuilder::new(field_type.clone(), date_type_option)
|
||||||
|
.name(name)
|
||||||
|
.visibility(true)
|
||||||
|
.build();
|
||||||
|
fields.push(date_field);
|
||||||
|
},
|
||||||
|
FieldType::LastEditedTime | FieldType::CreatedTime => {
|
||||||
|
// LastEditedTime and CreatedTime
|
||||||
|
let date_type_option = TimestampTypeOption {
|
||||||
|
date_format: DateFormat::US,
|
||||||
|
time_format: TimeFormat::TwentyFourHour,
|
||||||
|
include_time: true,
|
||||||
field_type: field_type.clone(),
|
field_type: field_type.clone(),
|
||||||
};
|
};
|
||||||
let name = match field_type {
|
let name = match field_type {
|
||||||
FieldType::DateTime => "Time",
|
FieldType::LastEditedTime => "Last Modified",
|
||||||
FieldType::LastEditedTime => "Updated At",
|
|
||||||
FieldType::CreatedTime => "Created At",
|
FieldType::CreatedTime => "Created At",
|
||||||
_ => "",
|
_ => "",
|
||||||
};
|
};
|
||||||
@ -128,7 +141,7 @@ pub fn make_test_board() -> DatabaseData {
|
|||||||
FieldType::RichText => row_builder.insert_text_cell("A"),
|
FieldType::RichText => row_builder.insert_text_cell("A"),
|
||||||
FieldType::Number => row_builder.insert_number_cell("1"),
|
FieldType::Number => row_builder.insert_number_cell("1"),
|
||||||
// 1647251762 => Mar 14,2022
|
// 1647251762 => Mar 14,2022
|
||||||
FieldType::DateTime | FieldType::LastEditedTime | FieldType::CreatedTime => {
|
FieldType::DateTime => {
|
||||||
row_builder.insert_date_cell(1647251762, None, None, &field_type)
|
row_builder.insert_date_cell(1647251762, None, None, &field_type)
|
||||||
},
|
},
|
||||||
FieldType::SingleSelect => {
|
FieldType::SingleSelect => {
|
||||||
@ -148,7 +161,7 @@ pub fn make_test_board() -> DatabaseData {
|
|||||||
FieldType::RichText => row_builder.insert_text_cell("B"),
|
FieldType::RichText => row_builder.insert_text_cell("B"),
|
||||||
FieldType::Number => row_builder.insert_number_cell("2"),
|
FieldType::Number => row_builder.insert_number_cell("2"),
|
||||||
// 1647251762 => Mar 14,2022
|
// 1647251762 => Mar 14,2022
|
||||||
FieldType::DateTime | FieldType::LastEditedTime | FieldType::CreatedTime => {
|
FieldType::DateTime => {
|
||||||
row_builder.insert_date_cell(1647251762, None, None, &field_type)
|
row_builder.insert_date_cell(1647251762, None, None, &field_type)
|
||||||
},
|
},
|
||||||
FieldType::SingleSelect => {
|
FieldType::SingleSelect => {
|
||||||
@ -167,7 +180,7 @@ pub fn make_test_board() -> DatabaseData {
|
|||||||
FieldType::RichText => row_builder.insert_text_cell("C"),
|
FieldType::RichText => row_builder.insert_text_cell("C"),
|
||||||
FieldType::Number => row_builder.insert_number_cell("3"),
|
FieldType::Number => row_builder.insert_number_cell("3"),
|
||||||
// 1647251762 => Mar 14,2022
|
// 1647251762 => Mar 14,2022
|
||||||
FieldType::DateTime | FieldType::LastEditedTime | FieldType::CreatedTime => {
|
FieldType::DateTime => {
|
||||||
row_builder.insert_date_cell(1647251762, None, None, &field_type)
|
row_builder.insert_date_cell(1647251762, None, None, &field_type)
|
||||||
},
|
},
|
||||||
FieldType::SingleSelect => {
|
FieldType::SingleSelect => {
|
||||||
@ -189,7 +202,7 @@ pub fn make_test_board() -> DatabaseData {
|
|||||||
match field_type {
|
match field_type {
|
||||||
FieldType::RichText => row_builder.insert_text_cell("DA"),
|
FieldType::RichText => row_builder.insert_text_cell("DA"),
|
||||||
FieldType::Number => row_builder.insert_number_cell("4"),
|
FieldType::Number => row_builder.insert_number_cell("4"),
|
||||||
FieldType::DateTime | FieldType::LastEditedTime | FieldType::CreatedTime => {
|
FieldType::DateTime => {
|
||||||
row_builder.insert_date_cell(1668704685, None, None, &field_type)
|
row_builder.insert_date_cell(1668704685, None, None, &field_type)
|
||||||
},
|
},
|
||||||
FieldType::SingleSelect => {
|
FieldType::SingleSelect => {
|
||||||
@ -206,7 +219,7 @@ pub fn make_test_board() -> DatabaseData {
|
|||||||
match field_type {
|
match field_type {
|
||||||
FieldType::RichText => row_builder.insert_text_cell("AE"),
|
FieldType::RichText => row_builder.insert_text_cell("AE"),
|
||||||
FieldType::Number => row_builder.insert_number_cell(""),
|
FieldType::Number => row_builder.insert_number_cell(""),
|
||||||
FieldType::DateTime | FieldType::LastEditedTime | FieldType::CreatedTime => {
|
FieldType::DateTime => {
|
||||||
row_builder.insert_date_cell(1668359085, None, None, &field_type)
|
row_builder.insert_date_cell(1668359085, None, None, &field_type)
|
||||||
},
|
},
|
||||||
FieldType::SingleSelect => {
|
FieldType::SingleSelect => {
|
||||||
|
@ -7,7 +7,7 @@ use flowy_database2::entities::FieldType;
|
|||||||
use flowy_database2::services::field::checklist_type_option::ChecklistTypeOption;
|
use flowy_database2::services::field::checklist_type_option::ChecklistTypeOption;
|
||||||
use flowy_database2::services::field::{
|
use flowy_database2::services::field::{
|
||||||
DateFormat, DateTypeOption, FieldBuilder, MultiSelectTypeOption, NumberFormat, NumberTypeOption,
|
DateFormat, DateTypeOption, FieldBuilder, MultiSelectTypeOption, NumberFormat, NumberTypeOption,
|
||||||
SelectOption, SelectOptionColor, SingleSelectTypeOption, TimeFormat,
|
SelectOption, SelectOptionColor, SingleSelectTypeOption, TimeFormat, TimestampTypeOption,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::database::database_editor::TestRowBuilder;
|
use crate::database::database_editor::TestRowBuilder;
|
||||||
@ -39,26 +39,39 @@ pub fn make_test_grid() -> DatabaseData {
|
|||||||
.build();
|
.build();
|
||||||
fields.push(number_field);
|
fields.push(number_field);
|
||||||
},
|
},
|
||||||
FieldType::DateTime | FieldType::LastEditedTime | FieldType::CreatedTime => {
|
FieldType::DateTime => {
|
||||||
// Date
|
// Date
|
||||||
let date_type_option = DateTypeOption {
|
let date_type_option = DateTypeOption {
|
||||||
date_format: DateFormat::US,
|
date_format: DateFormat::US,
|
||||||
time_format: TimeFormat::TwentyFourHour,
|
time_format: TimeFormat::TwentyFourHour,
|
||||||
timezone_id: "Etc/UTC".to_owned(),
|
timezone_id: "Etc/UTC".to_owned(),
|
||||||
field_type: field_type.clone(),
|
|
||||||
};
|
|
||||||
let name = match field_type {
|
|
||||||
FieldType::DateTime => "Time",
|
|
||||||
FieldType::LastEditedTime => "Updated At",
|
|
||||||
FieldType::CreatedTime => "Created At",
|
|
||||||
_ => "",
|
|
||||||
};
|
};
|
||||||
|
let name = "Time";
|
||||||
let date_field = FieldBuilder::new(field_type.clone(), date_type_option)
|
let date_field = FieldBuilder::new(field_type.clone(), date_type_option)
|
||||||
.name(name)
|
.name(name)
|
||||||
.visibility(true)
|
.visibility(true)
|
||||||
.build();
|
.build();
|
||||||
fields.push(date_field);
|
fields.push(date_field);
|
||||||
},
|
},
|
||||||
|
FieldType::LastEditedTime | FieldType::CreatedTime => {
|
||||||
|
// LastEditedTime and CreatedTime
|
||||||
|
let timestamp_type_option = TimestampTypeOption {
|
||||||
|
date_format: DateFormat::US,
|
||||||
|
time_format: TimeFormat::TwentyFourHour,
|
||||||
|
include_time: true,
|
||||||
|
field_type: field_type.clone(),
|
||||||
|
};
|
||||||
|
let name = match field_type {
|
||||||
|
FieldType::LastEditedTime => "Last Modified",
|
||||||
|
FieldType::CreatedTime => "Created At",
|
||||||
|
_ => "",
|
||||||
|
};
|
||||||
|
let timestamp_field = FieldBuilder::new(field_type.clone(), timestamp_type_option)
|
||||||
|
.name(name)
|
||||||
|
.visibility(true)
|
||||||
|
.build();
|
||||||
|
fields.push(timestamp_field);
|
||||||
|
},
|
||||||
FieldType::SingleSelect => {
|
FieldType::SingleSelect => {
|
||||||
// Single Select
|
// Single Select
|
||||||
let option1 = SelectOption::with_color(COMPLETED, SelectOptionColor::Purple);
|
let option1 = SelectOption::with_color(COMPLETED, SelectOptionColor::Purple);
|
||||||
@ -129,7 +142,7 @@ pub fn make_test_grid() -> DatabaseData {
|
|||||||
match field_type {
|
match field_type {
|
||||||
FieldType::RichText => row_builder.insert_text_cell("A"),
|
FieldType::RichText => row_builder.insert_text_cell("A"),
|
||||||
FieldType::Number => row_builder.insert_number_cell("1"),
|
FieldType::Number => row_builder.insert_number_cell("1"),
|
||||||
FieldType::DateTime | FieldType::LastEditedTime | FieldType::CreatedTime => {
|
FieldType::DateTime => {
|
||||||
row_builder.insert_date_cell(1647251762, None, None, &field_type)
|
row_builder.insert_date_cell(1647251762, None, None, &field_type)
|
||||||
},
|
},
|
||||||
FieldType::MultiSelect => row_builder
|
FieldType::MultiSelect => row_builder
|
||||||
@ -150,7 +163,7 @@ pub fn make_test_grid() -> DatabaseData {
|
|||||||
match field_type {
|
match field_type {
|
||||||
FieldType::RichText => row_builder.insert_text_cell(""),
|
FieldType::RichText => row_builder.insert_text_cell(""),
|
||||||
FieldType::Number => row_builder.insert_number_cell("2"),
|
FieldType::Number => row_builder.insert_number_cell("2"),
|
||||||
FieldType::DateTime | FieldType::LastEditedTime | FieldType::CreatedTime => {
|
FieldType::DateTime => {
|
||||||
row_builder.insert_date_cell(1647251762, None, None, &field_type)
|
row_builder.insert_date_cell(1647251762, None, None, &field_type)
|
||||||
},
|
},
|
||||||
FieldType::MultiSelect => row_builder
|
FieldType::MultiSelect => row_builder
|
||||||
@ -165,7 +178,7 @@ pub fn make_test_grid() -> DatabaseData {
|
|||||||
match field_type {
|
match field_type {
|
||||||
FieldType::RichText => row_builder.insert_text_cell("C"),
|
FieldType::RichText => row_builder.insert_text_cell("C"),
|
||||||
FieldType::Number => row_builder.insert_number_cell("3"),
|
FieldType::Number => row_builder.insert_number_cell("3"),
|
||||||
FieldType::DateTime | FieldType::LastEditedTime | FieldType::CreatedTime => {
|
FieldType::DateTime => {
|
||||||
row_builder.insert_date_cell(1647251762, None, None, &field_type)
|
row_builder.insert_date_cell(1647251762, None, None, &field_type)
|
||||||
},
|
},
|
||||||
FieldType::SingleSelect => {
|
FieldType::SingleSelect => {
|
||||||
@ -184,7 +197,7 @@ pub fn make_test_grid() -> DatabaseData {
|
|||||||
match field_type {
|
match field_type {
|
||||||
FieldType::RichText => row_builder.insert_text_cell("DA"),
|
FieldType::RichText => row_builder.insert_text_cell("DA"),
|
||||||
FieldType::Number => row_builder.insert_number_cell("14"),
|
FieldType::Number => row_builder.insert_number_cell("14"),
|
||||||
FieldType::DateTime | FieldType::LastEditedTime | FieldType::CreatedTime => {
|
FieldType::DateTime => {
|
||||||
row_builder.insert_date_cell(1668704685, None, None, &field_type)
|
row_builder.insert_date_cell(1668704685, None, None, &field_type)
|
||||||
},
|
},
|
||||||
FieldType::SingleSelect => {
|
FieldType::SingleSelect => {
|
||||||
@ -200,7 +213,7 @@ pub fn make_test_grid() -> DatabaseData {
|
|||||||
match field_type {
|
match field_type {
|
||||||
FieldType::RichText => row_builder.insert_text_cell("AE"),
|
FieldType::RichText => row_builder.insert_text_cell("AE"),
|
||||||
FieldType::Number => row_builder.insert_number_cell(""),
|
FieldType::Number => row_builder.insert_number_cell(""),
|
||||||
FieldType::DateTime | FieldType::LastEditedTime | FieldType::CreatedTime => {
|
FieldType::DateTime => {
|
||||||
row_builder.insert_date_cell(1668359085, None, None, &field_type)
|
row_builder.insert_date_cell(1668359085, None, None, &field_type)
|
||||||
},
|
},
|
||||||
FieldType::SingleSelect => {
|
FieldType::SingleSelect => {
|
||||||
@ -218,7 +231,7 @@ pub fn make_test_grid() -> DatabaseData {
|
|||||||
match field_type {
|
match field_type {
|
||||||
FieldType::RichText => row_builder.insert_text_cell("AE"),
|
FieldType::RichText => row_builder.insert_text_cell("AE"),
|
||||||
FieldType::Number => row_builder.insert_number_cell("5"),
|
FieldType::Number => row_builder.insert_number_cell("5"),
|
||||||
FieldType::DateTime | FieldType::LastEditedTime | FieldType::CreatedTime => {
|
FieldType::DateTime => {
|
||||||
row_builder.insert_date_cell(1671938394, None, None, &field_type)
|
row_builder.insert_date_cell(1671938394, None, None, &field_type)
|
||||||
},
|
},
|
||||||
FieldType::SingleSelect => {
|
FieldType::SingleSelect => {
|
||||||
|
@ -27,13 +27,13 @@ async fn export_csv_test() {
|
|||||||
let test = DatabaseEditorTest::new_grid().await;
|
let test = DatabaseEditorTest::new_grid().await;
|
||||||
let database = test.editor.clone();
|
let database = test.editor.clone();
|
||||||
let s = database.export_csv(CSVFormat::Original).await.unwrap();
|
let s = database.export_csv(CSVFormat::Original).await.unwrap();
|
||||||
let expected = r#"Name,Price,Time,Status,Platform,is urgent,link,TODO,Updated At,Created At
|
let expected = r#"Name,Price,Time,Status,Platform,is urgent,link,TODO,Last Modified,Created At
|
||||||
A,$1,2022/03/14,,"Google,Facebook",Yes,AppFlowy website - https://www.appflowy.io,,2022/03/14,2022/03/14
|
A,$1,2022/03/14,,"Google,Facebook",Yes,AppFlowy website - https://www.appflowy.io,,,
|
||||||
,$2,2022/03/14,,"Google,Twitter",Yes,,,2022/03/14,2022/03/14
|
,$2,2022/03/14,,"Google,Twitter",Yes,,,,
|
||||||
C,$3,2022/03/14,Completed,"Facebook,Google,Twitter",No,,,2022/03/14,2022/03/14
|
C,$3,2022/03/14,Completed,"Facebook,Google,Twitter",No,,,,
|
||||||
DA,$14,2022/11/17,Completed,,No,,,2022/11/17,2022/11/17
|
DA,$14,2022/11/17,Completed,,No,,,,
|
||||||
AE,,2022/11/13,Planned,"Facebook,Twitter",No,,,2022/11/13,2022/11/13
|
AE,,2022/11/13,Planned,"Facebook,Twitter",No,,,,
|
||||||
AE,$5,2022/12/25,Planned,Facebook,Yes,,,2022/12/25,2022/12/25
|
AE,$5,2022/12/25,Planned,Facebook,Yes,,,,
|
||||||
CB,,,,,,,,,
|
CB,,,,,,,,,
|
||||||
"#;
|
"#;
|
||||||
println!("{}", s);
|
println!("{}", s);
|
||||||
|
Reference in New Issue
Block a user