refactor: database field type option (#4136)

* refactor: include field type option in FieldPB

* refactor: adapt changes on flutter

* refactor: adapt changes on new tauri grid

* refactor: adapt changes on old tauri grid/board

* chore: merge
This commit is contained in:
Richard Shiue
2023-12-20 11:11:38 +08:00
committed by GitHub
parent 9a1ea138fc
commit d68c847d59
73 changed files with 1326 additions and 2459 deletions

View File

@ -1,5 +1,5 @@
import 'package:appflowy/plugins/database_view/grid/presentation/grid_page.dart'; import 'package:appflowy/plugins/database_view/grid/presentation/grid_page.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/type_option/select_option.dart'; import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/type_option/select/select_option.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-folder2/protobuf.dart'; import 'package:appflowy_backend/protobuf/flowy-folder2/protobuf.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';

View File

@ -28,8 +28,7 @@ import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/field_editor.dart'; import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/field_editor.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/field_type_extension.dart'; import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/field_type_extension.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/field_type_list.dart'; import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/field_type_list.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/field_type_option_editor.dart'; import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/type_option/date/date_time_format.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/type_option/date.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/row/row.dart'; import 'package:appflowy/plugins/database_view/grid/presentation/widgets/row/row.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/sort/create_sort_list.dart'; import 'package:appflowy/plugins/database_view/grid/presentation/widgets/sort/create_sort_list.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/sort/order_panel.dart'; import 'package:appflowy/plugins/database_view/grid/presentation/widgets/sort/order_panel.dart';
@ -61,7 +60,6 @@ import 'package:appflowy/plugins/database_view/widgets/setting/setting_property_
import 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/clear_date_button.dart'; import 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/clear_date_button.dart';
import 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/date_type_option_button.dart'; import 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/date_type_option_button.dart';
import 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/end_time_button.dart'; import 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/end_time_button.dart';
import 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/include_time_button.dart';
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart'; import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart';
import 'package:appflowy/workspace/presentation/widgets/toggle/toggle.dart'; import 'package:appflowy/workspace/presentation/widgets/toggle/toggle.dart';

View File

@ -4,13 +4,9 @@ import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
import 'package:appflowy/mobile/presentation/widgets/widgets.dart'; import 'package:appflowy/mobile/presentation/widgets/widgets.dart';
import 'package:appflowy/plugins/base/drag_handler.dart'; import 'package:appflowy/plugins/base/drag_handler.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/widgets/row/cells/date_cell/date_cell_editor_bloc.dart';
import 'package:appflowy/plugins/database_view/widgets/row/cells/date_cell/date_cal_bloc.dart';
import 'package:appflowy/plugins/database_view/widgets/row/cells/date_cell/mobile_date_editor.dart'; import 'package:appflowy/plugins/database_view/widgets/row/cells/date_cell/mobile_date_editor.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.pb.dart';
import 'package:dartz/dartz.dart' hide State;
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
@ -41,17 +37,6 @@ class MobileDateCellEditScreen extends StatefulWidget {
} }
class _MobileDateCellEditScreenState extends State<MobileDateCellEditScreen> { class _MobileDateCellEditScreenState extends State<MobileDateCellEditScreen> {
late final Future<Either<dynamic, FlowyError>> typeOptionFuture;
@override
void initState() {
super.initState();
typeOptionFuture = widget.controller.getTypeOption(
DateTypeOptionDataParser(),
);
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return widget.showAsFullScreen ? _buildFullScreen() : _buildNotFullScreen(); return widget.showAsFullScreen ? _buildFullScreen() : _buildNotFullScreen();
@ -64,7 +49,9 @@ class _MobileDateCellEditScreenState extends State<MobileDateCellEditScreen> {
LocaleKeys.titleBar_date.tr(), LocaleKeys.titleBar_date.tr(),
), ),
), ),
body: _buildBody(), body: _DateCellEditBody(
dateCellController: widget.controller,
),
); );
} }
@ -86,7 +73,9 @@ class _MobileDateCellEditScreenState extends State<MobileDateCellEditScreen> {
), ),
_buildHeader(), _buildHeader(),
Expanded( Expanded(
child: _buildBody(), child: _DateCellEditBody(
dateCellController: widget.controller,
),
), ),
], ],
), ),
@ -94,36 +83,6 @@ class _MobileDateCellEditScreenState extends State<MobileDateCellEditScreen> {
); );
} }
Widget _buildBody() {
return FutureBuilder<Either<dynamic, FlowyError>>(
future: typeOptionFuture,
builder: (context, snapshot) {
final data = snapshot.data;
if (data == null) {
return const Center(
child: CircularProgressIndicator.adaptive(),
);
}
return data.fold(
(dateTypeOptionPB) {
return _DateCellEditBody(
dateCellController: widget.controller,
dateTypeOptionPB: dateTypeOptionPB,
);
},
(err) {
Log.error(err);
return FlowyMobileStateContainer.error(
title: LocaleKeys.grid_field_failedToLoadDate.tr(),
errorMsg: err.toString(),
);
},
);
},
);
}
Widget _buildHeader() { Widget _buildHeader() {
const iconWidth = 30.0; const iconWidth = 30.0;
const height = 44.0; const height = 44.0;
@ -160,20 +119,16 @@ class _MobileDateCellEditScreenState extends State<MobileDateCellEditScreen> {
class _DateCellEditBody extends StatelessWidget { class _DateCellEditBody extends StatelessWidget {
const _DateCellEditBody({ const _DateCellEditBody({
required this.dateCellController, required this.dateCellController,
required this.dateTypeOptionPB,
}); });
final DateCellController dateCellController; final DateCellController dateCellController;
final DateTypeOptionPB dateTypeOptionPB;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return BlocProvider( return BlocProvider(
create: (context) => DateCellCalendarBloc( create: (context) => DateCellEditorBloc(
dateTypeOptionPB: dateTypeOptionPB,
cellData: dateCellController.getCellData(),
cellController: dateCellController, cellController: dateCellController,
)..add(const DateCellCalendarEvent.initial()), )..add(const DateCellEditorEvent.initial()),
child: const Column( child: const Column(
children: [ children: [
FlowyOptionDecorateBox( FlowyOptionDecorateBox(
@ -217,7 +172,7 @@ class _IncludeTimePickerState extends State<_IncludeTimePicker> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return BlocBuilder<DateCellCalendarBloc, DateCellCalendarState>( return BlocBuilder<DateCellEditorBloc, DateCellEditorState>(
builder: (context, state) { builder: (context, state) {
final startDay = state.dateStr; final startDay = state.dateStr;
final endDay = state.endDateStr; final endDay = state.endDateStr;
@ -306,7 +261,7 @@ class _IncludeTimePickerState extends State<_IncludeTimePicker> {
return GestureDetector( return GestureDetector(
onTap: () async { onTap: () async {
final bloc = context.read<DateCellCalendarBloc>(); final bloc = context.read<DateCellEditorBloc>();
await showMobileBottomSheet( await showMobileBottomSheet(
context, context,
builder: (context) { builder: (context) {
@ -331,8 +286,8 @@ class _IncludeTimePickerState extends State<_IncludeTimePicker> {
if (_selectedTime != null) { if (_selectedTime != null) {
bloc.add( bloc.add(
isStartDay isStartDay
? DateCellCalendarEvent.setTime(_selectedTime!) ? DateCellEditorEvent.setTime(_selectedTime!)
: DateCellCalendarEvent.setEndTime(_selectedTime!), : DateCellEditorEvent.setEndTime(_selectedTime!),
); );
} }
}, },
@ -361,7 +316,7 @@ class _EndDateSwitch extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return BlocSelector<DateCellCalendarBloc, DateCellCalendarState, bool>( return BlocSelector<DateCellEditorBloc, DateCellEditorState, bool>(
selector: (state) => state.isRange, selector: (state) => state.isRange,
builder: (context, isRange) { builder: (context, isRange) {
return FlowyOptionTile.toggle( return FlowyOptionTile.toggle(
@ -369,8 +324,8 @@ class _EndDateSwitch extends StatelessWidget {
isSelected: isRange, isSelected: isRange,
onValueChanged: (value) { onValueChanged: (value) {
context context
.read<DateCellCalendarBloc>() .read<DateCellEditorBloc>()
.add(DateCellCalendarEvent.setIsRange(value)); .add(DateCellEditorEvent.setIsRange(value));
}, },
); );
}, },
@ -383,7 +338,7 @@ class _IncludeTimeSwitch extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return BlocSelector<DateCellCalendarBloc, DateCellCalendarState, bool>( return BlocSelector<DateCellEditorBloc, DateCellEditorState, bool>(
selector: (state) => state.includeTime, selector: (state) => state.includeTime,
builder: (context, includeTime) { builder: (context, includeTime) {
return FlowyOptionTile.toggle( return FlowyOptionTile.toggle(
@ -392,8 +347,8 @@ class _IncludeTimeSwitch extends StatelessWidget {
isSelected: includeTime, isSelected: includeTime,
onValueChanged: (value) { onValueChanged: (value) {
context context
.read<DateCellCalendarBloc>() .read<DateCellEditorBloc>()
.add(DateCellCalendarEvent.setIncludeTime(value)); .add(DateCellEditorEvent.setIncludeTime(value));
}, },
); );
}, },
@ -420,7 +375,7 @@ class _TimeTextFieldState extends State<_TimeTextField> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return BlocConsumer<DateCellCalendarBloc, DateCellCalendarState>( return BlocConsumer<DateCellEditorBloc, DateCellEditorState>(
listener: (context, state) { listener: (context, state) {
_textController.text = _textController.text =
widget.isEndTime ? state.endTimeStr ?? "" : state.timeStr ?? ""; widget.isEndTime ? state.endTimeStr ?? "" : state.timeStr ?? "";
@ -437,10 +392,10 @@ class _TimeTextFieldState extends State<_TimeTextField> {
), ),
keyboardType: TextInputType.datetime, keyboardType: TextInputType.datetime,
onFieldSubmitted: (timeStr) { onFieldSubmitted: (timeStr) {
context.read<DateCellCalendarBloc>().add( context.read<DateCellEditorBloc>().add(
widget.isEndTime widget.isEndTime
? DateCellCalendarEvent.setEndTime(timeStr) ? DateCellEditorEvent.setEndTime(timeStr)
: DateCellCalendarEvent.setTime(timeStr), : DateCellEditorEvent.setTime(timeStr),
); );
}, },
); );
@ -463,8 +418,8 @@ class _ClearDateButton extends StatelessWidget {
return FlowyOptionTile.text( return FlowyOptionTile.text(
text: LocaleKeys.grid_field_clearDate.tr(), text: LocaleKeys.grid_field_clearDate.tr(),
onTap: () => context onTap: () => context
.read<DateCellCalendarBloc>() .read<DateCellEditorBloc>()
.add(const DateCellCalendarEvent.clearDate()), .add(const DateCellEditorEvent.clearDate()),
); );
} }
} }

View File

@ -28,19 +28,12 @@ class MobileEditPropertyScreen extends StatefulWidget {
} }
class _MobileEditPropertyScreenState extends State<MobileEditPropertyScreen> { class _MobileEditPropertyScreenState extends State<MobileEditPropertyScreen> {
late Future<FieldOptionValues?> future; late FieldOptionValues optionValues;
FieldOptionValues? optionValues;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
optionValues = FieldOptionValues.fromField(field: widget.field);
future = FieldOptionValues.get(
viewId: widget.viewId,
fieldId: widget.field.id,
fieldType: widget.field.fieldType,
);
} }
@override @override
@ -66,39 +59,30 @@ class _MobileEditPropertyScreenState extends State<MobileEditPropertyScreen> {
), ),
], ],
), ),
body: FutureBuilder<FieldOptionValues?>( body: FieldOptionEditor(
future: future, mode: FieldOptionMode.edit,
builder: (context, snapshot) { isPrimary: widget.field.isPrimary,
final optionValues = snapshot.data; defaultValues: optionValues,
if (optionValues == null) { onOptionValuesChanged: (optionValues) {
return const Center(child: CircularProgressIndicator.adaptive()); this.optionValues = optionValues;
} },
return FieldOptionEditor( onAction: (action) {
mode: FieldOptionMode.edit, final service = FieldServices(
isPrimary: widget.field.isPrimary, viewId: viewId,
defaultValues: optionValues, fieldId: fieldId,
onOptionValuesChanged: (optionValues) {
this.optionValues = optionValues;
},
onAction: (action) {
final service = FieldServices(
viewId: viewId,
fieldId: fieldId,
);
switch (action) {
case FieldOptionAction.delete:
service.delete();
break;
case FieldOptionAction.duplicate:
service.duplicate();
break;
case FieldOptionAction.hide:
service.hide();
break;
}
context.pop();
},
); );
switch (action) {
case FieldOptionAction.delete:
service.delete();
break;
case FieldOptionAction.duplicate:
service.duplicate();
break;
case FieldOptionAction.hide:
service.hide();
break;
}
context.pop();
}, },
), ),
); );

View File

@ -9,7 +9,7 @@ import 'package:appflowy/mobile/presentation/database/card/card_detail/widgets/w
import 'package:appflowy/mobile/presentation/widgets/widgets.dart'; import 'package:appflowy/mobile/presentation/widgets/widgets.dart';
import 'package:appflowy/plugins/database_view/application/field/field_service.dart'; import 'package:appflowy/plugins/database_view/application/field/field_service.dart';
import 'package:appflowy/plugins/database_view/application/field/type_option/number_format_bloc.dart'; import 'package:appflowy/plugins/database_view/application/field/type_option/number_format_bloc.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/type_option/date.dart'; import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/type_option/date/date_time_format.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/extension.dart';
import 'package:appflowy/util/field_type_extension.dart'; import 'package:appflowy/util/field_type_extension.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
@ -94,39 +94,30 @@ class FieldOptionValues {
} }
} }
static Future<FieldOptionValues?> get({ factory FieldOptionValues.fromField({
required String viewId, required FieldPB field,
required String fieldId, }) {
required FieldType fieldType, final fieldType = field.fieldType;
}) async { final buffer = field.typeOptionData;
final service = FieldBackendService(viewId: viewId, fieldId: fieldId); return FieldOptionValues(
final result = await service.getFieldTypeOptionData(fieldType: fieldType); type: fieldType,
return result.fold( name: field.name,
(option) { numberFormat: fieldType == FieldType.Number
final type = option.field_2.fieldType; ? NumberTypeOptionPB.fromBuffer(buffer).format
final buffer = option.typeOptionData; : null,
return FieldOptionValues( dateFormate: fieldType == FieldType.DateTime
type: type, ? DateTypeOptionPB.fromBuffer(buffer).dateFormat
name: option.field_2.name, : null,
numberFormat: type == FieldType.Number timeFormat: fieldType == FieldType.DateTime
? NumberTypeOptionPB.fromBuffer(buffer).format ? DateTypeOptionPB.fromBuffer(buffer).timeFormat
: null, : null,
dateFormate: type == FieldType.DateTime selectOption: switch (fieldType) {
? DateTypeOptionPB.fromBuffer(buffer).dateFormat FieldType.SingleSelect =>
: null, SingleSelectTypeOptionPB.fromBuffer(buffer).options,
timeFormat: type == FieldType.DateTime FieldType.MultiSelect =>
? DateTypeOptionPB.fromBuffer(buffer).timeFormat MultiSelectTypeOptionPB.fromBuffer(buffer).options,
: null, _ => [],
selectOption: switch (type) {
FieldType.SingleSelect =>
SingleSelectTypeOptionPB.fromBuffer(buffer).options,
FieldType.MultiSelect =>
MultiSelectTypeOptionPB.fromBuffer(buffer).options,
_ => [],
},
);
}, },
(error) => null,
); );
} }
} }

View File

@ -1,4 +1,5 @@
import 'dart:async'; import 'dart:async';
import 'package:appflowy/plugins/database_view/application/field/field_info.dart'; import 'package:appflowy/plugins/database_view/application/field/field_info.dart';
import 'package:appflowy/plugins/database_view/application/field/field_listener.dart'; import 'package:appflowy/plugins/database_view/application/field/field_listener.dart';
import 'package:appflowy/plugins/database_view/application/row/row_meta_listener.dart'; import 'package:appflowy/plugins/database_view/application/row/row_meta_listener.dart';
@ -9,8 +10,8 @@ import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:dartz/dartz.dart'; import 'package:dartz/dartz.dart';
import 'package:equatable/equatable.dart'; import 'package:equatable/equatable.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import '../field/field_service.dart';
import '../field/type_option/type_option_context.dart'; import '../field/type_option/type_option_data_parser.dart';
import 'cell_listener.dart'; import 'cell_listener.dart';
import 'cell_service.dart'; import 'cell_service.dart';
@ -26,7 +27,6 @@ class CellController<T, D> extends Equatable {
DatabaseCellContext _cellContext; DatabaseCellContext _cellContext;
final CellMemCache _cellCache; final CellMemCache _cellCache;
final CellCacheKey _cacheKey; final CellCacheKey _cacheKey;
final FieldBackendService _fieldBackendSvc;
final CellDataLoader<T> _cellDataLoader; final CellDataLoader<T> _cellDataLoader;
final CellDataPersistence<D> _cellDataPersistence; final CellDataPersistence<D> _cellDataPersistence;
@ -63,10 +63,6 @@ class CellController<T, D> extends Equatable {
_cellDataPersistence = cellDataPersistence, _cellDataPersistence = cellDataPersistence,
_rowMetaListener = RowMetaListener(cellContext.rowId), _rowMetaListener = RowMetaListener(cellContext.rowId),
_fieldListener = SingleFieldListener(fieldId: cellContext.fieldId), _fieldListener = SingleFieldListener(fieldId: cellContext.fieldId),
_fieldBackendSvc = FieldBackendService(
viewId: cellContext.viewId,
fieldId: cellContext.fieldInfo.id,
),
_cacheKey = CellCacheKey( _cacheKey = CellCacheKey(
rowId: cellContext.rowId, rowId: cellContext.rowId,
fieldId: cellContext.fieldInfo.id, fieldId: cellContext.fieldInfo.id,
@ -97,6 +93,9 @@ class CellController<T, D> extends Equatable {
/// For example: /// For example:
/// ¥12 -> $12 /// ¥12 -> $12
if (_cellDataLoader.reloadOnFieldChanged) { if (_cellDataLoader.reloadOnFieldChanged) {
_cellContext = _cellContext.copyWith(
fieldInfo: _cellContext.fieldInfo.copyWith(field: fieldPB),
);
_loadData(); _loadData();
} }
_onCellFieldChanged?.call(); _onCellFieldChanged?.call();
@ -148,17 +147,10 @@ class CellController<T, D> extends Equatable {
/// Return the TypeOptionPB that can be parsed into corresponding class using the [parser]. /// Return the TypeOptionPB that can be parsed into corresponding class using the [parser].
/// [PD] is the type that the parser return. /// [PD] is the type that the parser return.
Future<Either<PD, FlowyError>> getTypeOption<PD, P extends TypeOptionParser>( PD getTypeOption<PD, P extends TypeOptionParser>(
P parser, P parser,
) { ) {
return _fieldBackendSvc return parser.fromBuffer(_cellContext.fieldInfo.field.typeOptionData);
.getFieldTypeOptionData(fieldType: fieldType)
.then((result) {
return result.fold(
(data) => left(parser.fromBuffer(data.typeOptionData)),
(err) => right(err),
);
});
} }
/// Save the cell data to disk /// Save the cell data to disk

View File

@ -1,7 +1,8 @@
import 'dart:typed_data';
import 'package:appflowy/plugins/database_view/application/field_settings/field_settings_service.dart'; import 'package:appflowy/plugins/database_view/application/field_settings/field_settings_service.dart';
import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/field_settings_entities.pbenum.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:dartz/dartz.dart'; import 'package:dartz/dartz.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
@ -11,32 +12,24 @@ import 'field_controller.dart';
import 'field_info.dart'; import 'field_info.dart';
import 'field_listener.dart'; import 'field_listener.dart';
import 'field_service.dart'; import 'field_service.dart';
import 'type_option/type_option_context.dart';
import 'type_option/type_option_data_controller.dart';
part 'field_editor_bloc.freezed.dart'; part 'field_editor_bloc.freezed.dart';
class FieldEditorBloc extends Bloc<FieldEditorEvent, FieldEditorState> { class FieldEditorBloc extends Bloc<FieldEditorEvent, FieldEditorState> {
final FieldPB field;
final String viewId; final String viewId;
final String fieldId;
final FieldController fieldController; final FieldController fieldController;
final SingleFieldListener _singleFieldListener; final SingleFieldListener _singleFieldListener;
final FieldBackendService fieldService; final FieldBackendService fieldService;
final FieldSettingsBackendService fieldSettingsService; final FieldSettingsBackendService fieldSettingsService;
final TypeOptionController typeOptionController;
final void Function(String newFieldId)? onFieldInserted; final void Function(String newFieldId)? onFieldInserted;
FieldEditorBloc({ FieldEditorBloc({
required this.viewId, required this.viewId,
required this.field,
required this.fieldController, required this.fieldController,
this.onFieldInserted, this.onFieldInserted,
required FieldTypeOptionLoader loader, required FieldPB field,
}) : typeOptionController = TypeOptionController( }) : fieldId = field.id,
field: field,
loader: loader,
),
_singleFieldListener = SingleFieldListener(fieldId: field.id), _singleFieldListener = SingleFieldListener(fieldId: field.id),
fieldService = FieldBackendService( fieldService = FieldBackendService(
viewId: viewId, viewId: viewId,
@ -48,12 +41,6 @@ class FieldEditorBloc extends Bloc<FieldEditorEvent, FieldEditorState> {
(event, emit) async { (event, emit) async {
await event.when( await event.when(
initial: () async { initial: () async {
final fieldId = field.id;
typeOptionController.addFieldListener((field) {
if (!isClosed) {
add(FieldEditorEvent.didReceiveFieldChanged(fieldId));
}
});
_singleFieldListener.start( _singleFieldListener.start(
onFieldChanged: (field) { onFieldChanged: (field) {
if (!isClosed) { if (!isClosed) {
@ -61,7 +48,6 @@ class FieldEditorBloc extends Bloc<FieldEditorEvent, FieldEditorState> {
} }
}, },
); );
await typeOptionController.reloadTypeOption();
add(FieldEditorEvent.didReceiveFieldChanged(fieldId)); add(FieldEditorEvent.didReceiveFieldChanged(fieldId));
}, },
didReceiveFieldChanged: (fieldId) async { didReceiveFieldChanged: (fieldId) async {
@ -69,23 +55,31 @@ class FieldEditorBloc extends Bloc<FieldEditorEvent, FieldEditorState> {
emit(state.copyWith(field: fieldController.getField(fieldId)!)); emit(state.copyWith(field: fieldController.getField(fieldId)!));
}, },
switchFieldType: (fieldType) async { switchFieldType: (fieldType) async {
await typeOptionController.switchToField(fieldType); await fieldService.updateFieldType(fieldType: fieldType);
}, },
renameField: (newName) async { renameField: (newName) async {
final result = await fieldService.updateField(name: newName); final result = await fieldService.updateField(name: newName);
_logIfError(result); _logIfError(result);
}, },
updateTypeOption: (typeOptionData) async {
final result = await FieldBackendService.updateFieldTypeOption(
viewId: viewId,
fieldId: fieldId,
typeOptionData: typeOptionData,
);
_logIfError(result);
},
insertLeft: () async { insertLeft: () async {
final result = await fieldService.insertBefore(); final result = await fieldService.insertBefore();
result.fold( result.fold(
(typeOptionPB) => onFieldInserted?.call(typeOptionPB.field_2.id), (newField) => onFieldInserted?.call(newField.id),
(err) => Log.error("Failed creating field $err"), (err) => Log.error("Failed creating field $err"),
); );
}, },
insertRight: () async { insertRight: () async {
final result = await fieldService.insertAfter(); final result = await fieldService.insertAfter();
result.fold( result.fold(
(typeOptionPB) => onFieldInserted?.call(typeOptionPB.field_2.id), (newField) => onFieldInserted?.call(newField.id),
(err) => Log.error("Failed creating field $err"), (err) => Log.error("Failed creating field $err"),
); );
}, },
@ -129,6 +123,9 @@ class FieldEditorEvent with _$FieldEditorEvent {
_DidReceiveFieldChanged; _DidReceiveFieldChanged;
const factory FieldEditorEvent.switchFieldType(final FieldType fieldType) = const factory FieldEditorEvent.switchFieldType(final FieldType fieldType) =
_SwitchFieldType; _SwitchFieldType;
const factory FieldEditorEvent.updateTypeOption(
final Uint8List typeOptionData,
) = _UpdateTypeOption;
const factory FieldEditorEvent.renameField(final String name) = _RenameField; const factory FieldEditorEvent.renameField(final String name) = _RenameField;
const factory FieldEditorEvent.insertLeft() = _InsertLeft; const factory FieldEditorEvent.insertLeft() = _InsertLeft;
const factory FieldEditorEvent.insertRight() = _InsertRight; const factory FieldEditorEvent.insertRight() = _InsertRight;

View File

@ -15,7 +15,7 @@ class FieldBackendService {
final String viewId; final String viewId;
final String fieldId; final String fieldId;
static Future<Either<TypeOptionPB, FlowyError>> createField({ static Future<Either<FieldPB, FlowyError>> createField({
required String viewId, required String viewId,
FieldType fieldType = FieldType.RichText, FieldType fieldType = FieldType.RichText,
String? fieldName, String? fieldName,
@ -33,7 +33,7 @@ class FieldBackendService {
return DatabaseEventCreateField(payload).send(); return DatabaseEventCreateField(payload).send();
} }
Future<Either<TypeOptionPB, FlowyError>> insertBefore({ Future<Either<FieldPB, FlowyError>> insertBefore({
FieldType fieldType = FieldType.RichText, FieldType fieldType = FieldType.RichText,
String? fieldName, String? fieldName,
Uint8List? typeOptionData, Uint8List? typeOptionData,
@ -50,7 +50,7 @@ class FieldBackendService {
); );
} }
Future<Either<TypeOptionPB, FlowyError>> insertAfter({ Future<Either<FieldPB, FlowyError>> insertAfter({
FieldType fieldType = FieldType.RichText, FieldType fieldType = FieldType.RichText,
String? fieldName, String? fieldName,
Uint8List? typeOptionData, Uint8List? typeOptionData,
@ -61,7 +61,7 @@ class FieldBackendService {
fieldName: fieldName, fieldName: fieldName,
typeOptionData: typeOptionData, typeOptionData: typeOptionData,
position: OrderObjectPositionPB( position: OrderObjectPositionPB(
position: OrderObjectPositionTypePB.Before, position: OrderObjectPositionTypePB.After,
objectId: fieldId, objectId: fieldId,
), ),
); );
@ -156,21 +156,6 @@ class FieldBackendService {
return duplicateField(viewId: viewId, fieldId: fieldId); return duplicateField(viewId: viewId, fieldId: fieldId);
} }
Future<Either<TypeOptionPB, FlowyError>> getFieldTypeOptionData({
required FieldType fieldType,
}) {
final payload = TypeOptionPathPB.create()
..viewId = viewId
..fieldId = fieldId
..fieldType = fieldType;
return DatabaseEventGetTypeOption(payload).send().then((result) {
return result.fold(
(data) => left(data),
(err) => right(err),
);
});
}
/// Returns the primary field of the view. /// Returns the primary field of the view.
static Future<Either<FieldPB, FlowyError>> getPrimaryField({ static Future<Either<FieldPB, FlowyError>> getPrimaryField({
required String viewId, required String viewId,

View File

@ -1,66 +0,0 @@
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'dart:async';
import 'type_option/type_option_data_controller.dart';
part 'field_type_option_edit_bloc.freezed.dart';
class FieldTypeOptionEditBloc
extends Bloc<FieldTypeOptionEditEvent, FieldTypeOptionEditState> {
final TypeOptionController _dataController;
void Function()? _fieldListenFn;
FieldTypeOptionEditBloc(TypeOptionController dataController)
: _dataController = dataController,
super(FieldTypeOptionEditState.initial(dataController)) {
on<FieldTypeOptionEditEvent>(
(event, emit) async {
event.when(
initial: () {
_fieldListenFn = dataController.addFieldListener((field) {
add(FieldTypeOptionEditEvent.didReceiveFieldUpdated(field));
});
},
didReceiveFieldUpdated: (field) {
emit(state.copyWith(field: field));
},
switchToField: (FieldType fieldType) async {
await _dataController.switchToField(fieldType);
},
);
},
);
}
@override
Future<void> close() async {
if (_fieldListenFn != null) {
_dataController.removeFieldListener(_fieldListenFn!);
}
return super.close();
}
}
@freezed
class FieldTypeOptionEditEvent with _$FieldTypeOptionEditEvent {
const factory FieldTypeOptionEditEvent.initial() = _Initial;
const factory FieldTypeOptionEditEvent.switchToField(FieldType fieldType) =
_SwitchToField;
const factory FieldTypeOptionEditEvent.didReceiveFieldUpdated(FieldPB field) =
_DidReceiveFieldUpdated;
}
@freezed
class FieldTypeOptionEditState with _$FieldTypeOptionEditState {
const factory FieldTypeOptionEditState({
required FieldPB field,
}) = _FieldTypeOptionEditState;
factory FieldTypeOptionEditState.initial(
TypeOptionController typeOptionController,
) =>
FieldTypeOptionEditState(
field: typeOptionController.field,
);
}

View File

@ -1,68 +0,0 @@
import 'package:appflowy_backend/protobuf/flowy-database2/date_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 'date_bloc.freezed.dart';
class DateTypeOptionBloc
extends Bloc<DateTypeOptionEvent, DateTypeOptionState> {
DateTypeOptionBloc({required DateTypeOptionContext typeOptionContext})
: super(DateTypeOptionState.initial(typeOptionContext.typeOption)) {
on<DateTypeOptionEvent>(
(event, emit) async {
event.map(
didSelectDateFormat: (_DidSelectDateFormat value) {
emit(
state.copyWith(
typeOption: _updateTypeOption(dateFormat: value.format),
),
);
},
didSelectTimeFormat: (_DidSelectTimeFormat value) {
emit(
state.copyWith(
typeOption: _updateTypeOption(timeFormat: value.format),
),
);
},
);
},
);
}
DateTypeOptionPB _updateTypeOption({
DateFormatPB? dateFormat,
TimeFormatPB? timeFormat,
}) {
state.typeOption.freeze();
return state.typeOption.rebuild((typeOption) {
if (dateFormat != null) {
typeOption.dateFormat = dateFormat;
}
if (timeFormat != null) {
typeOption.timeFormat = timeFormat;
}
});
}
}
@freezed
class DateTypeOptionEvent with _$DateTypeOptionEvent {
const factory DateTypeOptionEvent.didSelectDateFormat(DateFormatPB format) =
_DidSelectDateFormat;
const factory DateTypeOptionEvent.didSelectTimeFormat(TimeFormatPB format) =
_DidSelectTimeFormat;
}
@freezed
class DateTypeOptionState with _$DateTypeOptionState {
const factory DateTypeOptionState({
required DateTypeOptionPB typeOption,
}) = _DateTypeOptionState;
factory DateTypeOptionState.initial(DateTypeOptionPB typeOption) =>
DateTypeOptionState(typeOption: typeOption);
}

View File

@ -1,85 +1,81 @@
import 'dart:async';
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/type_option/builder.dart';
import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/select_option.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/select_option.pb.dart';
import 'dart:async';
import 'select_option_type_option_bloc.dart';
import 'type_option_context.dart';
import 'type_option_service.dart';
import 'package:protobuf/protobuf.dart';
class MultiSelectAction with ISelectOptionAction { import 'select_option_type_option_bloc.dart';
final String viewId; import 'type_option_service.dart';
final String fieldId;
class MultiSelectAction implements ISelectOptionAction {
final TypeOptionBackendService service; final TypeOptionBackendService service;
final MultiSelectTypeOptionContext typeOptionContext; final TypeOptionDataCallback onTypeOptionUpdated;
MultiSelectAction({ MultiSelectAction({
required this.viewId, required this.onTypeOptionUpdated,
required this.fieldId, required String viewId,
required this.typeOptionContext, required String fieldId,
}) : service = TypeOptionBackendService( }) : service = TypeOptionBackendService(viewId: viewId, fieldId: fieldId);
viewId: viewId,
fieldId: fieldId,
);
MultiSelectTypeOptionPB get typeOption => typeOptionContext.typeOption; @override
Future<List<SelectOptionPB>> insertOption(
List<SelectOptionPB> options,
String optionName,
) {
final newOptions = List<SelectOptionPB>.from(options);
return service.newOption(name: optionName).then((result) {
return result.fold(
(option) {
final exists =
newOptions.any((element) => element.name == option.name);
if (!exists) {
newOptions.insert(0, option);
}
set typeOption(MultiSelectTypeOptionPB newTypeOption) { _updateTypeOption(newOptions);
typeOptionContext.typeOption = newTypeOption; return newOptions;
},
(err) {
Log.error(err);
return newOptions;
},
);
});
} }
@override @override
List<SelectOptionPB> Function(SelectOptionPB) get deleteOption { List<SelectOptionPB> deleteOption(
return (SelectOptionPB option) { List<SelectOptionPB> options,
typeOption.freeze(); SelectOptionPB deletedOption,
typeOption = typeOption.rebuild((typeOption) { ) {
final index = final newOptions = List<SelectOptionPB>.from(options);
typeOption.options.indexWhere((element) => element.id == option.id); final index =
if (index != -1) { newOptions.indexWhere((option) => option.id == deletedOption.id);
typeOption.options.removeAt(index); if (index != -1) {
} newOptions.removeAt(index);
}); }
return typeOption.options;
}; _updateTypeOption(newOptions);
return newOptions;
} }
@override @override
Future<List<SelectOptionPB>> Function(String) get insertOption { List<SelectOptionPB> updateOption(
return (String optionName) { List<SelectOptionPB> options,
return service.newOption(name: optionName).then((result) { SelectOptionPB option,
return result.fold( ) {
(option) { final newOptions = List<SelectOptionPB>.from(options);
typeOption.freeze(); final index = newOptions.indexWhere((element) => element.id == option.id);
typeOption = typeOption.rebuild((typeOption) { if (index != -1) {
final exists = typeOption.options newOptions[index] = option;
.any((element) => element.name == option.name); }
if (!exists) {
typeOption.options.insert(0, option);
}
});
return typeOption.options; _updateTypeOption(newOptions);
}, return newOptions;
(err) {
Log.error(err);
return typeOption.options;
},
);
});
};
} }
@override void _updateTypeOption(List<SelectOptionPB> options) {
List<SelectOptionPB> Function(SelectOptionPB) get updateOption { final newTypeOption = MultiSelectTypeOptionPB()..options.addAll(options);
return (SelectOptionPB option) { onTypeOptionUpdated(newTypeOption.writeToBuffer());
typeOption.freeze();
typeOption = typeOption.rebuild((typeOption) {
final index =
typeOption.options.indexWhere((element) => element.id == option.id);
if (index != -1) {
typeOption.options[index] = option;
}
});
return typeOption.options;
};
} }
} }

View File

@ -1,48 +0,0 @@
import 'package:appflowy_backend/protobuf/flowy-database2/number_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 'number_bloc.freezed.dart';
class NumberTypeOptionBloc
extends Bloc<NumberTypeOptionEvent, NumberTypeOptionState> {
NumberTypeOptionBloc({required NumberTypeOptionContext typeOptionContext})
: super(NumberTypeOptionState.initial(typeOptionContext.typeOption)) {
on<NumberTypeOptionEvent>(
(event, emit) async {
event.map(
didSelectFormat: (_DidSelectFormat value) {
emit(state.copyWith(typeOption: _updateNumberFormat(value.format)));
},
);
},
);
}
NumberTypeOptionPB _updateNumberFormat(NumberFormatPB format) {
state.typeOption.freeze();
return state.typeOption.rebuild((typeOption) {
typeOption.format = format;
});
}
}
@freezed
class NumberTypeOptionEvent with _$NumberTypeOptionEvent {
const factory NumberTypeOptionEvent.didSelectFormat(NumberFormatPB format) =
_DidSelectFormat;
}
@freezed
class NumberTypeOptionState with _$NumberTypeOptionState {
const factory NumberTypeOptionState({
required NumberTypeOptionPB typeOption,
}) = _NumberTypeOptionState;
factory NumberTypeOptionState.initial(NumberTypeOptionPB typeOption) =>
NumberTypeOptionState(
typeOption: typeOption,
);
}

View File

@ -5,12 +5,21 @@ import 'dart:async';
import 'package:dartz/dartz.dart'; import 'package:dartz/dartz.dart';
part 'select_option_type_option_bloc.freezed.dart'; part 'select_option_type_option_bloc.freezed.dart';
abstract mixin class ISelectOptionAction { abstract class ISelectOptionAction {
Future<List<SelectOptionPB>> Function(String) get insertOption; Future<List<SelectOptionPB>> insertOption(
List<SelectOptionPB> options,
String newOptionName,
);
List<SelectOptionPB> Function(SelectOptionPB) get deleteOption; List<SelectOptionPB> deleteOption(
List<SelectOptionPB> options,
SelectOptionPB deletedOption,
);
List<SelectOptionPB> Function(SelectOptionPB) get updateOption; List<SelectOptionPB> updateOption(
List<SelectOptionPB> options,
SelectOptionPB updatedOption,
);
} }
class SelectOptionTypeOptionBloc class SelectOptionTypeOptionBloc
@ -26,7 +35,7 @@ class SelectOptionTypeOptionBloc
await event.when( await event.when(
createOption: (optionName) async { createOption: (optionName) async {
final List<SelectOptionPB> options = final List<SelectOptionPB> options =
await typeOptionAction.insertOption(optionName); await typeOptionAction.insertOption(state.options, optionName);
emit(state.copyWith(options: options)); emit(state.copyWith(options: options));
}, },
addingOption: () { addingOption: () {
@ -37,12 +46,12 @@ class SelectOptionTypeOptionBloc
}, },
updateOption: (option) { updateOption: (option) {
final List<SelectOptionPB> options = final List<SelectOptionPB> options =
typeOptionAction.updateOption(option); typeOptionAction.updateOption(state.options, option);
emit(state.copyWith(options: options)); emit(state.copyWith(options: options));
}, },
deleteOption: (option) { deleteOption: (option) {
final List<SelectOptionPB> options = final List<SelectOptionPB> options =
typeOptionAction.deleteOption(option); typeOptionAction.deleteOption(state.options, option);
emit(state.copyWith(options: options)); emit(state.copyWith(options: options));
}, },
); );

View File

@ -1,82 +1,83 @@
import 'dart:async';
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/type_option/builder.dart';
import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/select_option.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/select_option.pb.dart';
import 'dart:async';
import 'package:protobuf/protobuf.dart';
import 'select_option_type_option_bloc.dart'; import 'select_option_type_option_bloc.dart';
import 'type_option_context.dart';
import 'type_option_service.dart'; import 'type_option_service.dart';
class SingleSelectAction with ISelectOptionAction { class SingleSelectAction implements ISelectOptionAction {
final String viewId;
final String fieldId;
final SingleSelectTypeOptionContext typeOptionContext;
final TypeOptionBackendService service; final TypeOptionBackendService service;
final TypeOptionDataCallback onTypeOptionUpdated;
SingleSelectAction({ SingleSelectAction({
required this.viewId, required this.onTypeOptionUpdated,
required this.fieldId, required String viewId,
required this.typeOptionContext, required String fieldId,
}) : service = TypeOptionBackendService(viewId: viewId, fieldId: fieldId); }) : service = TypeOptionBackendService(viewId: viewId, fieldId: fieldId);
SingleSelectTypeOptionPB get typeOption => typeOptionContext.typeOption; @override
Future<List<SelectOptionPB>> insertOption(
List<SelectOptionPB> options,
String optionName,
) {
final newOptions = List<SelectOptionPB>.from(options);
return service.newOption(name: optionName).then((result) {
return result.fold(
(option) {
final exists =
newOptions.any((element) => element.name == option.name);
if (!exists) {
newOptions.insert(0, option);
}
set typeOption(SingleSelectTypeOptionPB newTypeOption) { _updateTypeOption(newOptions);
typeOptionContext.typeOption = newTypeOption; return newOptions;
},
(err) {
Log.error(err);
return newOptions;
},
);
});
} }
@override @override
List<SelectOptionPB> Function(SelectOptionPB) get deleteOption { List<SelectOptionPB> deleteOption(
return (SelectOptionPB option) { List<SelectOptionPB> options,
typeOption.freeze(); SelectOptionPB deletedOption,
typeOption = typeOption.rebuild((typeOption) { ) {
final index = final newOptions = List<SelectOptionPB>.from(options);
typeOption.options.indexWhere((element) => element.id == option.id); final index =
if (index != -1) { newOptions.indexWhere((option) => option.id == deletedOption.id);
typeOption.options.removeAt(index); if (index != -1) {
} newOptions.removeAt(index);
}); }
return typeOption.options;
}; final newTypeOption = MultiSelectTypeOptionPB()..options.addAll(newOptions);
onTypeOptionUpdated(newTypeOption.writeToBuffer());
return newOptions;
} }
@override @override
Future<List<SelectOptionPB>> Function(String) get insertOption { List<SelectOptionPB> updateOption(
return (String optionName) { List<SelectOptionPB> options,
return service.newOption(name: optionName).then((result) { SelectOptionPB updatedOption,
return result.fold( ) {
(option) { final newOptions = List<SelectOptionPB>.from(options);
typeOption.freeze(); final index =
typeOption = typeOption.rebuild((typeOption) { newOptions.indexWhere((option) => option.id == updatedOption.id);
final exists = typeOption.options if (index != -1) {
.any((element) => element.name == option.name); newOptions[index] = updatedOption;
if (!exists) { }
typeOption.options.insert(0, option);
}
});
return typeOption.options; _updateTypeOption(newOptions);
}, return newOptions;
(err) {
Log.error(err);
return typeOption.options;
},
);
});
};
} }
@override void _updateTypeOption(List<SelectOptionPB> options) {
List<SelectOptionPB> Function(SelectOptionPB) get updateOption { final newTypeOption = SingleSelectTypeOptionPB()..options.addAll(options);
return (SelectOptionPB option) { onTypeOptionUpdated(newTypeOption.writeToBuffer());
typeOption.freeze();
typeOption = typeOption.rebuild((typeOption) {
final index =
typeOption.options.indexWhere((element) => element.id == option.id);
if (index != -1) {
typeOption.options[index] = option;
}
});
return typeOption.options;
};
} }
} }

View File

@ -1,76 +0,0 @@
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);
}

View File

@ -1,190 +0,0 @@
import 'package:appflowy_backend/dispatch/dispatch.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/checkbox_entities.pb.dart';
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/select_option.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-error/errors.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart';
import 'package:dartz/dartz.dart';
import 'package:protobuf/protobuf.dart';
import 'type_option_data_controller.dart';
abstract class TypeOptionParser<T> {
T fromBuffer(List<int> buffer);
}
// Number
typedef NumberTypeOptionContext = TypeOptionContext<NumberTypeOptionPB>;
class NumberTypeOptionWidgetDataParser
extends TypeOptionParser<NumberTypeOptionPB> {
@override
NumberTypeOptionPB fromBuffer(List<int> buffer) {
return NumberTypeOptionPB.fromBuffer(buffer);
}
}
// RichText
typedef RichTextTypeOptionContext = TypeOptionContext<RichTextTypeOptionPB>;
class RichTextTypeOptionWidgetDataParser
extends TypeOptionParser<RichTextTypeOptionPB> {
@override
RichTextTypeOptionPB fromBuffer(List<int> buffer) {
return RichTextTypeOptionPB.fromBuffer(buffer);
}
}
// Checkbox
typedef CheckboxTypeOptionContext = TypeOptionContext<CheckboxTypeOptionPB>;
class CheckboxTypeOptionWidgetDataParser
extends TypeOptionParser<CheckboxTypeOptionPB> {
@override
CheckboxTypeOptionPB fromBuffer(List<int> buffer) {
return CheckboxTypeOptionPB.fromBuffer(buffer);
}
}
// URL
typedef URLTypeOptionContext = TypeOptionContext<URLTypeOptionPB>;
class URLTypeOptionWidgetDataParser extends TypeOptionParser<URLTypeOptionPB> {
@override
URLTypeOptionPB fromBuffer(List<int> buffer) {
return URLTypeOptionPB.fromBuffer(buffer);
}
}
// DateTime
typedef DateTypeOptionContext = TypeOptionContext<DateTypeOptionPB>;
class DateTypeOptionDataParser extends TypeOptionParser<DateTypeOptionPB> {
@override
DateTypeOptionPB fromBuffer(List<int> buffer) {
return DateTypeOptionPB.fromBuffer(buffer);
}
}
// LastModified and CreatedAt
typedef TimestampTypeOptionContext = TypeOptionContext<TimestampTypeOptionPB>;
class TimestampTypeOptionDataParser
extends TypeOptionParser<TimestampTypeOptionPB> {
@override
TimestampTypeOptionPB fromBuffer(List<int> buffer) {
return TimestampTypeOptionPB.fromBuffer(buffer);
}
}
// SingleSelect
typedef SingleSelectTypeOptionContext
= TypeOptionContext<SingleSelectTypeOptionPB>;
class SingleSelectTypeOptionWidgetDataParser
extends TypeOptionParser<SingleSelectTypeOptionPB> {
@override
SingleSelectTypeOptionPB fromBuffer(List<int> buffer) {
return SingleSelectTypeOptionPB.fromBuffer(buffer);
}
}
// Multi-select
typedef MultiSelectTypeOptionContext
= TypeOptionContext<MultiSelectTypeOptionPB>;
class MultiSelectTypeOptionWidgetDataParser
extends TypeOptionParser<MultiSelectTypeOptionPB> {
@override
MultiSelectTypeOptionPB fromBuffer(List<int> buffer) {
return MultiSelectTypeOptionPB.fromBuffer(buffer);
}
}
// Multi-select
typedef ChecklistTypeOptionContext = TypeOptionContext<ChecklistTypeOptionPB>;
class ChecklistTypeOptionWidgetDataParser
extends TypeOptionParser<ChecklistTypeOptionPB> {
@override
ChecklistTypeOptionPB fromBuffer(List<int> buffer) {
return ChecklistTypeOptionPB.fromBuffer(buffer);
}
}
class TypeOptionContext<T extends GeneratedMessage> {
T? _typeOptionObject;
final TypeOptionParser<T> dataParser;
final TypeOptionController _dataController;
TypeOptionContext({
required this.dataParser,
required TypeOptionController dataController,
}) : _dataController = dataController;
String get viewId => _dataController.loader.viewId;
String get fieldId => _dataController.field.id;
Future<T> loadTypeOptionData({
void Function(T)? onCompleted,
required void Function(FlowyError) onError,
}) async {
await _dataController.reloadTypeOption().then((result) {
result.fold((l) => null, (err) => onError(err));
});
onCompleted?.call(typeOption);
return typeOption;
}
T get typeOption {
if (_typeOptionObject != null) {
return _typeOptionObject!;
}
final T object = _dataController.getTypeOption(dataParser);
_typeOptionObject = object;
return object;
}
set typeOption(T typeOption) {
_dataController.typeOptionData = typeOption.writeToBuffer();
_typeOptionObject = typeOption;
}
}
abstract class TypeOptionFieldDelegate {
void onFieldChanged(void Function(String) callback);
void dispose();
}
abstract class ITypeOptionLoader {
String get viewId;
String get fieldName;
Future<Either<TypeOptionPB, FlowyError>> initialize();
}
/// Uses when editing a existing field
class FieldTypeOptionLoader {
final String viewId;
final FieldPB field;
FieldTypeOptionLoader({
required this.viewId,
required this.field,
});
Future<Either<TypeOptionPB, FlowyError>> load() {
final payload = TypeOptionPathPB.create()
..viewId = viewId
..fieldId = field.id
..fieldType = field.fieldType;
return DatabaseEventGetTypeOption(payload).send();
}
}

View File

@ -1,117 +0,0 @@
import 'package:appflowy_backend/dispatch/dispatch.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:dartz/dartz.dart';
import 'package:flowy_infra/notifier.dart';
import 'package:protobuf/protobuf.dart' hide FieldInfo;
import '../field_service.dart';
import 'type_option_context.dart';
class TypeOptionController {
late TypeOptionPB _typeOption;
final FieldTypeOptionLoader loader;
final PublishNotifier<FieldPB> _fieldNotifier = PublishNotifier();
/// Returns a [TypeOptionController] used to modify the specified
/// [FieldPB]'s data
///
/// Should call [reloadTypeOption] if the passed-in [FieldInfo]
/// is null
///
TypeOptionController({
required this.loader,
required FieldPB field,
}) {
_typeOption = TypeOptionPB.create()
..viewId = loader.viewId
..field_2 = field;
}
Future<Either<TypeOptionPB, FlowyError>> reloadTypeOption() async {
final result = await loader.load();
return result.fold(
(data) {
data.freeze();
_typeOption = data;
_fieldNotifier.value = data.field_2;
return left(data);
},
(err) {
Log.error(err);
return right(err);
},
);
}
FieldPB get field {
return _typeOption.field_2;
}
T getTypeOption<T>(TypeOptionParser<T> parser) {
return parser.fromBuffer(_typeOption.typeOptionData);
}
set fieldName(String name) {
_typeOption = _typeOption.rebuild((rebuildData) {
rebuildData.field_2 = rebuildData.field_2.rebuild((rebuildField) {
rebuildField.name = name;
});
});
_fieldNotifier.value = _typeOption.field_2;
FieldBackendService(viewId: loader.viewId, fieldId: field.id)
.updateField(name: name);
}
set typeOptionData(List<int> typeOptionData) {
_typeOption = _typeOption.rebuild((rebuildData) {
if (typeOptionData.isNotEmpty) {
rebuildData.typeOptionData = typeOptionData;
}
});
FieldBackendService.updateFieldTypeOption(
viewId: loader.viewId,
fieldId: field.id,
typeOptionData: typeOptionData,
);
}
Future<void> switchToField(FieldType newFieldType) async {
final payload = UpdateFieldTypePayloadPB.create()
..viewId = loader.viewId
..fieldId = field.id
..fieldType = newFieldType;
final result = await DatabaseEventUpdateFieldType(payload).send();
await result.fold(
(_) async {
// Should load the type-option data after switching to a new field.
// After loading the type-option data, the editor widget that uses
// the type-option data will be rebuild.
await reloadTypeOption();
},
(err) => Future(() => Log.error(err)),
);
}
void Function() addFieldListener(void Function(FieldPB) callback) {
void listener() {
callback(field);
}
_fieldNotifier.addListener(listener);
return listener;
}
void removeFieldListener(void Function() listener) {
_fieldNotifier.removeListener(listener);
}
void dispose() {
_fieldNotifier.dispose();
}
}

View File

@ -0,0 +1,80 @@
import 'package:appflowy_backend/protobuf/flowy-database2/checkbox_entities.pb.dart';
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/select_option.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';
abstract class TypeOptionParser<T> {
T fromBuffer(List<int> buffer);
}
class RichTextTypeOptionDataParser
extends TypeOptionParser<RichTextTypeOptionPB> {
@override
RichTextTypeOptionPB fromBuffer(List<int> buffer) {
return RichTextTypeOptionPB.fromBuffer(buffer);
}
}
class NumberTypeOptionDataParser extends TypeOptionParser<NumberTypeOptionPB> {
@override
NumberTypeOptionPB fromBuffer(List<int> buffer) {
return NumberTypeOptionPB.fromBuffer(buffer);
}
}
class CheckboxTypeOptionDataParser
extends TypeOptionParser<CheckboxTypeOptionPB> {
@override
CheckboxTypeOptionPB fromBuffer(List<int> buffer) {
return CheckboxTypeOptionPB.fromBuffer(buffer);
}
}
class URLTypeOptionDataParser extends TypeOptionParser<URLTypeOptionPB> {
@override
URLTypeOptionPB fromBuffer(List<int> buffer) {
return URLTypeOptionPB.fromBuffer(buffer);
}
}
class DateTypeOptionDataParser extends TypeOptionParser<DateTypeOptionPB> {
@override
DateTypeOptionPB fromBuffer(List<int> buffer) {
return DateTypeOptionPB.fromBuffer(buffer);
}
}
class TimestampTypeOptionDataParser
extends TypeOptionParser<TimestampTypeOptionPB> {
@override
TimestampTypeOptionPB fromBuffer(List<int> buffer) {
return TimestampTypeOptionPB.fromBuffer(buffer);
}
}
class SingleSelectTypeOptionDataParser
extends TypeOptionParser<SingleSelectTypeOptionPB> {
@override
SingleSelectTypeOptionPB fromBuffer(List<int> buffer) {
return SingleSelectTypeOptionPB.fromBuffer(buffer);
}
}
class MultiSelectTypeOptionDataParser
extends TypeOptionParser<MultiSelectTypeOptionPB> {
@override
MultiSelectTypeOptionPB fromBuffer(List<int> buffer) {
return MultiSelectTypeOptionPB.fromBuffer(buffer);
}
}
class ChecklistTypeOptionDataParser
extends TypeOptionParser<ChecklistTypeOptionPB> {
@override
ChecklistTypeOptionPB fromBuffer(List<int> buffer) {
return ChecklistTypeOptionPB.fromBuffer(buffer);
}
}

View File

@ -90,17 +90,16 @@ class SelectOptionFilterEditorBloc
} }
void _loadOptions() { void _loadOptions() {
delegate.loadOptions().then((options) { if (!isClosed) {
if (!isClosed) { final options = delegate.loadOptions();
String filterDesc = ''; String filterDesc = '';
for (final option in options) { for (final option in options) {
if (state.filter.optionIds.contains(option.id)) { if (state.filter.optionIds.contains(option.id)) {
filterDesc += "${option.name} "; filterDesc += "${option.name} ";
}
} }
add(SelectOptionFilterEditorEvent.updateFilterDescription(filterDesc));
} }
}); add(SelectOptionFilterEditorEvent.updateFilterDescription(filterDesc));
}
} }
@override @override

View File

@ -100,11 +100,10 @@ class SelectOptionFilterListBloc<T>
} }
void _loadOptions() { void _loadOptions() {
delegate.loadOptions().then((options) { if (!isClosed) {
if (!isClosed) { final options = delegate.loadOptions();
add(SelectOptionFilterListEvent.didReceiveOptions(options)); add(SelectOptionFilterListEvent.didReceiveOptions(options));
} }
});
} }
void _startListening() {} void _startListening() {}

View File

@ -32,14 +32,16 @@ class SelectOptionFilterList extends StatelessWidget {
viewId: filterInfo.viewId, viewId: filterInfo.viewId,
fieldPB: filterInfo.fieldInfo.field, fieldPB: filterInfo.fieldInfo.field,
selectedOptionIds: selectedOptionIds, selectedOptionIds: selectedOptionIds,
delegate: SingleSelectOptionFilterDelegateImpl(filterInfo), delegate:
SingleSelectOptionFilterDelegateImpl(filterInfo: filterInfo),
); );
} else { } else {
bloc = SelectOptionFilterListBloc( bloc = SelectOptionFilterListBloc(
viewId: filterInfo.viewId, viewId: filterInfo.viewId,
fieldPB: filterInfo.fieldInfo.field, fieldPB: filterInfo.fieldInfo.field,
selectedOptionIds: selectedOptionIds, selectedOptionIds: selectedOptionIds,
delegate: MultiSelectOptionFilterDelegateImpl(filterInfo), delegate:
MultiSelectOptionFilterDelegateImpl(filterInfo: filterInfo),
); );
} }

View File

@ -31,12 +31,14 @@ class _SelectOptionFilterChoicechipState
if (widget.filterInfo.fieldInfo.fieldType == FieldType.SingleSelect) { if (widget.filterInfo.fieldInfo.fieldType == FieldType.SingleSelect) {
bloc = SelectOptionFilterEditorBloc( bloc = SelectOptionFilterEditorBloc(
filterInfo: widget.filterInfo, filterInfo: widget.filterInfo,
delegate: SingleSelectOptionFilterDelegateImpl(widget.filterInfo), delegate:
SingleSelectOptionFilterDelegateImpl(filterInfo: widget.filterInfo),
); );
} else { } else {
bloc = SelectOptionFilterEditorBloc( bloc = SelectOptionFilterEditorBloc(
filterInfo: widget.filterInfo, filterInfo: widget.filterInfo,
delegate: MultiSelectOptionFilterDelegateImpl(widget.filterInfo), delegate:
MultiSelectOptionFilterDelegateImpl(filterInfo: widget.filterInfo),
); );
} }
bloc.add(const SelectOptionFilterEditorEvent.initial()); bloc.add(const SelectOptionFilterEditorEvent.initial());

View File

@ -1,50 +1,35 @@
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_data_parser.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/type_option/builder.dart'; import 'package:appflowy/plugins/database_view/grid/presentation/widgets/filter/filter_info.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/select_option.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/select_option.pb.dart';
import '../../filter_info.dart';
abstract class SelectOptionFilterDelegate { abstract class SelectOptionFilterDelegate {
Future<List<SelectOptionPB>> loadOptions(); List<SelectOptionPB> loadOptions();
} }
class SingleSelectOptionFilterDelegateImpl class SingleSelectOptionFilterDelegateImpl
implements SelectOptionFilterDelegate { implements SelectOptionFilterDelegate {
final SingleSelectTypeOptionContext typeOptionContext; final FilterInfo filterInfo;
SingleSelectOptionFilterDelegateImpl(FilterInfo filterInfo) SingleSelectOptionFilterDelegateImpl({
: typeOptionContext = makeSingleSelectTypeOptionContext( required this.filterInfo,
viewId: filterInfo.viewId, });
fieldPB: filterInfo.fieldInfo.field,
);
@override @override
Future<List<SelectOptionPB>> loadOptions() { List<SelectOptionPB> loadOptions() {
return typeOptionContext final parser = SingleSelectTypeOptionDataParser();
.loadTypeOptionData( return parser.fromBuffer(filterInfo.fieldInfo.field.typeOptionData).options;
onError: (error) => Log.error(error),
)
.then((value) => value.options);
} }
} }
class MultiSelectOptionFilterDelegateImpl class MultiSelectOptionFilterDelegateImpl
implements SelectOptionFilterDelegate { implements SelectOptionFilterDelegate {
final MultiSelectTypeOptionContext typeOptionContext; final FilterInfo filterInfo;
MultiSelectOptionFilterDelegateImpl(FilterInfo filterInfo) MultiSelectOptionFilterDelegateImpl({required this.filterInfo});
: typeOptionContext = makeMultiSelectTypeOptionContext(
viewId: filterInfo.viewId,
fieldPB: filterInfo.fieldInfo.field,
);
@override @override
Future<List<SelectOptionPB>> loadOptions() { List<SelectOptionPB> loadOptions() {
return typeOptionContext final parser = MultiSelectTypeOptionDataParser();
.loadTypeOptionData( return parser.fromBuffer(filterInfo.fieldInfo.field.typeOptionData).options;
onError: (error) => Log.error(error),
)
.then((value) => value.options);
} }
} }

View File

@ -1,5 +0,0 @@
import 'package:flutter/material.dart';
class GridHeaderConstants {
static Color get backgroundColor => Colors.grey;
}

View File

@ -1,12 +1,14 @@
import 'dart:typed_data';
import 'package:appflowy/generated/flowy_svgs.g.dart'; 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/field/field_controller.dart'; import 'package:appflowy/plugins/database_view/application/field/field_controller.dart';
import 'package:appflowy/plugins/database_view/application/field/field_editor_bloc.dart'; import 'package:appflowy/plugins/database_view/application/field/field_editor_bloc.dart';
import 'package:appflowy/plugins/database_view/application/field/field_info.dart'; import 'package:appflowy/plugins/database_view/application/field/field_info.dart';
import 'package:appflowy/plugins/database_view/application/field/field_service.dart'; import 'package:appflowy/plugins/database_view/application/field/field_service.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/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/common/type_option_separator.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/field_type_extension.dart';
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/field_settings_entities.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/field_settings_entities.pb.dart';
@ -18,7 +20,8 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:styled_widget/styled_widget.dart'; import 'package:styled_widget/styled_widget.dart';
import 'field_type_option_editor.dart'; import 'field_type_list.dart';
import 'type_option/builder.dart';
enum FieldEditorPage { enum FieldEditorPage {
general, general,
@ -70,10 +73,6 @@ class _FieldEditorState extends State<FieldEditor> {
field: widget.field, field: widget.field,
fieldController: widget.fieldController, fieldController: widget.fieldController,
onFieldInserted: widget.onFieldInserted, onFieldInserted: widget.onFieldInserted,
loader: FieldTypeOptionLoader(
viewId: widget.viewId,
field: widget.field,
),
)..add(const FieldEditorEvent.initial()), )..add(const FieldEditorEvent.initial()),
child: _currentPage == FieldEditorPage.details child: _currentPage == FieldEditorPage.details
? _fieldDetails() ? _fieldDetails()
@ -326,12 +325,13 @@ class _FieldDetailsEditorState extends State<FieldDetailsEditor> {
final List<Widget> children = [ final List<Widget> children = [
FieldNameTextField( FieldNameTextField(
popoverMutex: popoverMutex, popoverMutex: popoverMutex,
padding: const EdgeInsets.fromLTRB(12.0, 4.0, 12.0, 0.0), padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 4.0),
textEditingController: widget.textEditingController, textEditingController: widget.textEditingController,
), ),
const VSpace(8), const VSpace(8.0),
FieldTypeOptionCell(popoverMutex: popoverMutex), SwitchFieldButton(popoverMutex: popoverMutex),
const TypeOptionSeparator(), const TypeOptionSeparator(spacing: 8.0),
FieldTypeOptionEditor(viewId: widget.viewId, popoverMutex: popoverMutex),
_addFieldVisibilityToggleButton(), _addFieldVisibilityToggleButton(),
_addDuplicateFieldButton(), _addDuplicateFieldButton(),
_addDeleteFieldButton(), _addDeleteFieldButton(),
@ -350,7 +350,7 @@ class _FieldDetailsEditorState extends State<FieldDetailsEditor> {
return BlocBuilder<FieldEditorBloc, FieldEditorState>( return BlocBuilder<FieldEditorBloc, FieldEditorState>(
builder: (context, state) { builder: (context, state) {
return Padding( return Padding(
padding: const EdgeInsets.fromLTRB(8.0, 2.0, 8.0, 0), padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: FieldActionCell( child: FieldActionCell(
viewId: widget.viewId, viewId: widget.viewId,
fieldInfo: state.field, fieldInfo: state.field,
@ -401,11 +401,13 @@ class _FieldDetailsEditorState extends State<FieldDetailsEditor> {
} }
} }
class FieldTypeOptionCell extends StatelessWidget { class FieldTypeOptionEditor extends StatelessWidget {
final String viewId;
final PopoverMutex popoverMutex; final PopoverMutex popoverMutex;
const FieldTypeOptionCell({ const FieldTypeOptionEditor({
super.key, super.key,
required this.viewId,
required this.popoverMutex, required this.popoverMutex,
}); });
@ -416,14 +418,28 @@ class FieldTypeOptionCell extends StatelessWidget {
if (state.field.isPrimary) { if (state.field.isPrimary) {
return const SizedBox.shrink(); return const SizedBox.shrink();
} }
final dataController = final typeOptionEditor = makeTypeOptionEditor(
context.read<FieldEditorBloc>().typeOptionController; context: context,
return Padding( viewId: viewId,
padding: const EdgeInsets.only(bottom: 2.0), field: state.field.field,
child: FieldTypeOptionEditor( popoverMutex: popoverMutex,
dataController: dataController, onTypeOptionUpdated: (Uint8List typeOptionData) {
popoverMutex: popoverMutex, context
), .read<FieldEditorBloc>()
.add(FieldEditorEvent.updateTypeOption(typeOptionData));
},
);
if (typeOptionEditor == null) {
return const SizedBox.shrink();
}
return Column(
mainAxisSize: MainAxisSize.min,
children: [
typeOptionEditor,
const TypeOptionSeparator(spacing: 8.0),
],
); );
}, },
); );
@ -493,3 +509,58 @@ class _FieldNameTextFieldState extends State<FieldNameTextField> {
super.dispose(); super.dispose();
} }
} }
class SwitchFieldButton extends StatefulWidget {
final PopoverMutex popoverMutex;
const SwitchFieldButton({
super.key,
required this.popoverMutex,
});
@override
State<SwitchFieldButton> createState() => _SwitchFieldButtonState();
}
class _SwitchFieldButtonState extends State<SwitchFieldButton> {
final PopoverController _popoverController = PopoverController();
@override
Widget build(BuildContext context) {
return SizedBox(
height: GridSize.popoverItemHeight,
child: AppFlowyPopover(
constraints: BoxConstraints.loose(const Size(460, 540)),
triggerActions: PopoverTriggerFlags.hover,
mutex: widget.popoverMutex,
controller: _popoverController,
offset: const Offset(8, 0),
margin: const EdgeInsets.all(8),
popupBuilder: (BuildContext popoverContext) {
return FieldTypeList(
onSelectField: (newFieldType) {
context
.read<FieldEditorBloc>()
.add(FieldEditorEvent.switchFieldType(newFieldType));
},
);
},
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: _buildMoreButton(context),
),
),
);
}
Widget _buildMoreButton(BuildContext context) {
final bloc = context.read<FieldEditorBloc>();
return FlowyButton(
onTap: () => _popoverController.show(),
text: FlowyText.medium(
bloc.state.field.fieldType.title(),
),
leftIcon: FlowySvg(bloc.state.field.fieldType.icon()),
rightIcon: const FlowySvg(FlowySvgs.more_s),
);
}
}

View File

@ -1,135 +0,0 @@
import 'dart:typed_data';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/plugins/database_view/application/field/field_type_option_edit_bloc.dart';
import 'package:appflowy/plugins/database_view/application/field/type_option/type_option_data_controller.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/layout/sizes.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:dartz/dartz.dart' show Either;
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'field_type_extension.dart';
import 'field_type_list.dart';
import 'type_option/builder.dart';
typedef UpdateFieldCallback = void Function(FieldPB, Uint8List);
typedef SwitchToFieldCallback = Future<Either<TypeOptionPB, FlowyError>>
Function(
String fieldId,
FieldType fieldType,
);
class FieldTypeOptionEditor extends StatelessWidget {
final TypeOptionController _dataController;
final PopoverMutex popoverMutex;
const FieldTypeOptionEditor({
super.key,
required TypeOptionController dataController,
required this.popoverMutex,
}) : _dataController = dataController;
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) {
return FieldTypeOptionEditBloc(_dataController)
..add(const FieldTypeOptionEditEvent.initial());
},
child: BlocBuilder<FieldTypeOptionEditBloc, FieldTypeOptionEditState>(
builder: (context, state) {
final typeOptionWidget = _typeOptionWidget(
context: context,
state: state,
);
final List<Widget> children = [
SwitchFieldButton(popoverMutex: popoverMutex),
if (typeOptionWidget != null) typeOptionWidget,
];
return Column(
mainAxisSize: MainAxisSize.min,
children: children,
);
},
),
);
}
Widget? _typeOptionWidget({
required BuildContext context,
required FieldTypeOptionEditState state,
}) {
return makeTypeOptionWidget(
context: context,
dataController: _dataController,
popoverMutex: popoverMutex,
);
}
}
class SwitchFieldButton extends StatefulWidget {
final PopoverMutex popoverMutex;
const SwitchFieldButton({
super.key,
required this.popoverMutex,
});
@override
State<SwitchFieldButton> createState() => _SwitchFieldButtonState();
}
class _SwitchFieldButtonState extends State<SwitchFieldButton> {
final PopoverController _popoverController = PopoverController();
@override
Widget build(BuildContext context) {
final child = AppFlowyPopover(
constraints: BoxConstraints.loose(const Size(460, 540)),
triggerActions: PopoverTriggerFlags.hover,
mutex: widget.popoverMutex,
controller: _popoverController,
offset: const Offset(8, 0),
margin: const EdgeInsets.all(8),
popupBuilder: (BuildContext popoverContext) {
return FieldTypeList(
onSelectField: (newFieldType) {
context
.read<FieldTypeOptionEditBloc>()
.add(FieldTypeOptionEditEvent.switchToField(newFieldType));
},
);
},
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: _buildMoreButton(context),
),
);
return SizedBox(
height: GridSize.popoverItemHeight,
child: child,
);
}
Widget _buildMoreButton(BuildContext context) {
final bloc = context.read<FieldTypeOptionEditBloc>();
return FlowyButton(
onTap: () => _popoverController.show(),
text: FlowyText.medium(
bloc.state.field.fieldType.title(),
),
leftIcon: FlowySvg(bloc.state.field.fieldType.icon()),
rightIcon: const FlowySvg(FlowySvgs.more_s),
);
}
}
abstract class TypeOptionWidget extends StatelessWidget {
const TypeOptionWidget({super.key});
}

View File

@ -196,7 +196,7 @@ class _CreateFieldButtonState extends State<CreateFieldButton> {
viewId: widget.viewId, viewId: widget.viewId,
); );
result.fold( result.fold(
(typeOptionPB) => widget.onFieldCreated(typeOptionPB.field_2.id), (field) => widget.onFieldCreated(field.id),
(err) => Log.error("Failed to create field type option: $err"), (err) => Log.error("Failed to create field type option: $err"),
); );
}, },

View File

@ -1,19 +1,9 @@
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:appflowy/plugins/database_view/application/field/field_info.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.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_data_controller.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/checkbox_entities.pb.dart';
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/select_option.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_popover/appflowy_popover.dart'; import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:protobuf/protobuf.dart' hide FieldInfo;
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'checkbox.dart'; import 'checkbox.dart';
import 'checklist.dart'; import 'checklist.dart';
import 'date.dart'; import 'date.dart';
@ -24,237 +14,47 @@ import 'single_select.dart';
import 'timestamp.dart'; import 'timestamp.dart';
import 'url.dart'; import 'url.dart';
typedef TypeOptionData = Uint8List; typedef TypeOptionDataCallback = void Function(Uint8List typeOptionData);
typedef TypeOptionDataCallback = void Function(TypeOptionData typeOptionData);
typedef ShowOverlayCallback = void Function(
BuildContext anchorContext,
Widget child, {
VoidCallback? onRemoved,
});
typedef HideOverlayCallback = void Function(BuildContext anchorContext);
class TypeOptionOverlayDelegate { abstract class TypeOptionEditorFactory {
ShowOverlayCallback showOverlay; Widget? build({
HideOverlayCallback hideOverlay; required BuildContext context,
TypeOptionOverlayDelegate({ required String viewId,
required this.showOverlay, required FieldPB field,
required this.hideOverlay, required PopoverMutex popoverMutex,
required TypeOptionDataCallback onTypeOptionUpdated,
}); });
factory TypeOptionEditorFactory.makeBuilder(FieldType fieldType) {
return switch (fieldType) {
FieldType.RichText => const RichTextTypeOptionEditorFactory(),
FieldType.Number => const NumberTypeOptionEditorFactory(),
FieldType.URL => const URLTypeOptionEditorFactory(),
FieldType.DateTime => const DateTypeOptionEditorFactory(),
FieldType.LastEditedTime => const TimestampTypeOptionEditorFactory(),
FieldType.CreatedTime => const TimestampTypeOptionEditorFactory(),
FieldType.SingleSelect => const SingleSelectTypeOptionEditorFactory(),
FieldType.MultiSelect => const MultiSelectTypeOptionEditorFactory(),
FieldType.Checkbox => const CheckboxTypeOptionEditorFactory(),
FieldType.Checklist => const ChecklistTypeOptionEditorFactory(),
_ => throw UnimplementedError(),
};
}
} }
abstract class TypeOptionWidgetBuilder { Widget? makeTypeOptionEditor({
Widget? build(BuildContext context);
}
Widget? makeTypeOptionWidget({
required BuildContext context, required BuildContext context,
required TypeOptionController dataController, required String viewId,
required FieldPB field,
required PopoverMutex popoverMutex, required PopoverMutex popoverMutex,
required TypeOptionDataCallback onTypeOptionUpdated,
}) { }) {
final builder = makeTypeOptionWidgetBuilder( final editorBuilder = TypeOptionEditorFactory.makeBuilder(field.fieldType);
dataController: dataController, return editorBuilder.build(
context: context,
viewId: viewId,
field: field,
onTypeOptionUpdated: onTypeOptionUpdated,
popoverMutex: popoverMutex, popoverMutex: popoverMutex,
); );
return builder.build(context);
}
TypeOptionWidgetBuilder makeTypeOptionWidgetBuilder({
required TypeOptionController dataController,
required PopoverMutex popoverMutex,
}) {
final viewId = dataController.loader.viewId;
final fieldType = dataController.field.fieldType;
switch (dataController.field.fieldType) {
case FieldType.Checkbox:
return CheckboxTypeOptionWidgetBuilder(
makeTypeOptionContextWithDataController<CheckboxTypeOptionPB>(
viewId: viewId,
fieldType: fieldType,
dataController: dataController,
),
);
case FieldType.DateTime:
return DateTypeOptionWidgetBuilder(
makeTypeOptionContextWithDataController<DateTypeOptionPB>(
viewId: viewId,
fieldType: fieldType,
dataController: dataController,
),
popoverMutex,
);
case FieldType.LastEditedTime:
case FieldType.CreatedTime:
return TimestampTypeOptionWidgetBuilder(
makeTypeOptionContextWithDataController<TimestampTypeOptionPB>(
viewId: viewId,
fieldType: fieldType,
dataController: dataController,
),
popoverMutex,
);
case FieldType.SingleSelect:
return SingleSelectTypeOptionWidgetBuilder(
makeTypeOptionContextWithDataController<SingleSelectTypeOptionPB>(
viewId: viewId,
fieldType: fieldType,
dataController: dataController,
),
popoverMutex,
);
case FieldType.MultiSelect:
return MultiSelectTypeOptionWidgetBuilder(
makeTypeOptionContextWithDataController<MultiSelectTypeOptionPB>(
viewId: viewId,
fieldType: fieldType,
dataController: dataController,
),
popoverMutex,
);
case FieldType.Number:
return NumberTypeOptionWidgetBuilder(
makeTypeOptionContextWithDataController<NumberTypeOptionPB>(
viewId: viewId,
fieldType: fieldType,
dataController: dataController,
),
popoverMutex,
);
case FieldType.RichText:
return RichTextTypeOptionWidgetBuilder(
makeTypeOptionContextWithDataController<RichTextTypeOptionPB>(
viewId: viewId,
fieldType: fieldType,
dataController: dataController,
),
);
case FieldType.URL:
return URLTypeOptionWidgetBuilder(
makeTypeOptionContextWithDataController<URLTypeOptionPB>(
viewId: viewId,
fieldType: fieldType,
dataController: dataController,
),
);
case FieldType.Checklist:
return ChecklistTypeOptionWidgetBuilder(
makeTypeOptionContextWithDataController<ChecklistTypeOptionPB>(
viewId: viewId,
fieldType: fieldType,
dataController: dataController,
),
);
}
throw UnimplementedError;
}
TypeOptionContext<T> makeTypeOptionContext<T extends GeneratedMessage>({
required String viewId,
required FieldInfo fieldInfo,
}) {
final loader = FieldTypeOptionLoader(viewId: viewId, field: fieldInfo.field);
final dataController = TypeOptionController(
loader: loader,
field: fieldInfo.field,
);
return makeTypeOptionContextWithDataController(
viewId: viewId,
fieldType: fieldInfo.fieldType,
dataController: dataController,
);
}
TypeOptionContext<SingleSelectTypeOptionPB> makeSingleSelectTypeOptionContext({
required String viewId,
required FieldPB fieldPB,
}) {
return makeSelectTypeOptionContext(viewId: viewId, fieldPB: fieldPB);
}
TypeOptionContext<MultiSelectTypeOptionPB> makeMultiSelectTypeOptionContext({
required String viewId,
required FieldPB fieldPB,
}) {
return makeSelectTypeOptionContext(viewId: viewId, fieldPB: fieldPB);
}
TypeOptionContext<T> makeSelectTypeOptionContext<T extends GeneratedMessage>({
required String viewId,
required FieldPB fieldPB,
}) {
final loader = FieldTypeOptionLoader(
viewId: viewId,
field: fieldPB,
);
final dataController = TypeOptionController(
loader: loader,
field: fieldPB,
);
final typeOptionContext = makeTypeOptionContextWithDataController<T>(
viewId: viewId,
fieldType: fieldPB.fieldType,
dataController: dataController,
);
return typeOptionContext;
}
TypeOptionContext<T>
makeTypeOptionContextWithDataController<T extends GeneratedMessage>({
required String viewId,
required FieldType fieldType,
required TypeOptionController dataController,
}) {
switch (fieldType) {
case FieldType.Checkbox:
return CheckboxTypeOptionContext(
dataController: dataController,
dataParser: CheckboxTypeOptionWidgetDataParser(),
) as TypeOptionContext<T>;
case FieldType.DateTime:
return DateTypeOptionContext(
dataController: dataController,
dataParser: DateTypeOptionDataParser(),
) as TypeOptionContext<T>;
case FieldType.LastEditedTime:
case FieldType.CreatedTime:
return TimestampTypeOptionContext(
dataController: dataController,
dataParser: TimestampTypeOptionDataParser(),
) as TypeOptionContext<T>;
case FieldType.SingleSelect:
return SingleSelectTypeOptionContext(
dataController: dataController,
dataParser: SingleSelectTypeOptionWidgetDataParser(),
) as TypeOptionContext<T>;
case FieldType.MultiSelect:
return MultiSelectTypeOptionContext(
dataController: dataController,
dataParser: MultiSelectTypeOptionWidgetDataParser(),
) as TypeOptionContext<T>;
case FieldType.Checklist:
return ChecklistTypeOptionContext(
dataController: dataController,
dataParser: ChecklistTypeOptionWidgetDataParser(),
) as TypeOptionContext<T>;
case FieldType.Number:
return NumberTypeOptionContext(
dataController: dataController,
dataParser: NumberTypeOptionWidgetDataParser(),
) as TypeOptionContext<T>;
case FieldType.RichText:
return RichTextTypeOptionContext(
dataController: dataController,
dataParser: RichTextTypeOptionWidgetDataParser(),
) as TypeOptionContext<T>;
case FieldType.URL:
return URLTypeOptionContext(
dataController: dataController,
dataParser: URLTypeOptionWidgetDataParser(),
) as TypeOptionContext<T>;
}
throw UnimplementedError;
} }

View File

@ -1,10 +1,19 @@
import 'package:appflowy/plugins/database_view/application/field/type_option/type_option_context.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'builder.dart'; import 'builder.dart';
class CheckboxTypeOptionWidgetBuilder extends TypeOptionWidgetBuilder { class CheckboxTypeOptionEditorFactory implements TypeOptionEditorFactory {
CheckboxTypeOptionWidgetBuilder(CheckboxTypeOptionContext typeOptionContext); const CheckboxTypeOptionEditorFactory();
@override @override
Widget? build(BuildContext context) => null; Widget? build({
required BuildContext context,
required String viewId,
required FieldPB field,
required PopoverMutex popoverMutex,
required TypeOptionDataCallback onTypeOptionUpdated,
}) =>
null;
} }

View File

@ -1,12 +1,19 @@
import 'package:appflowy/plugins/database_view/application/field/type_option/type_option_context.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'builder.dart'; import 'builder.dart';
class ChecklistTypeOptionWidgetBuilder extends TypeOptionWidgetBuilder { class ChecklistTypeOptionEditorFactory implements TypeOptionEditorFactory {
ChecklistTypeOptionWidgetBuilder( const ChecklistTypeOptionEditorFactory();
ChecklistTypeOptionContext typeOptionContext,
);
@override @override
Widget? build(BuildContext context) => null; Widget? build({
required BuildContext context,
required String viewId,
required FieldPB field,
required PopoverMutex popoverMutex,
required TypeOptionDataCallback onTypeOptionUpdated,
}) =>
null;
} }

View File

@ -1,80 +1,49 @@
import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/plugins/database_view/application/field/type_option/type_option_data_parser.dart';
import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:appflowy/plugins/database_view/application/field/type_option/date_bloc.dart';
import 'package:appflowy/plugins/database_view/application/field/type_option/type_option_context.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/date_entities.pbenum.dart';
import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:easy_localization/easy_localization.dart' hide DateFormat;
import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:protobuf/protobuf.dart';
import '../../../layout/sizes.dart'; import '../../../layout/sizes.dart';
import '../../common/type_option_separator.dart';
import '../field_type_option_editor.dart';
import 'builder.dart'; import 'builder.dart';
import 'date/date_time_format.dart';
class DateTypeOptionWidgetBuilder extends TypeOptionWidgetBuilder { class DateTypeOptionEditorFactory implements TypeOptionEditorFactory {
final DateTypeOptionWidget _widget; const DateTypeOptionEditorFactory();
DateTypeOptionWidgetBuilder(
DateTypeOptionContext typeOptionContext,
PopoverMutex popoverMutex,
) : _widget = DateTypeOptionWidget(
typeOptionContext: typeOptionContext,
popoverMutex: popoverMutex,
);
@override @override
Widget? build(BuildContext context) { Widget? build({
return _widget; required BuildContext context,
} required String viewId,
} required FieldPB field,
required PopoverMutex popoverMutex,
required TypeOptionDataCallback onTypeOptionUpdated,
}) {
final typeOption = _parseTypeOptionData(field.typeOptionData);
class DateTypeOptionWidget extends TypeOptionWidget { return Column(
final DateTypeOptionContext typeOptionContext; mainAxisSize: MainAxisSize.min,
final PopoverMutex popoverMutex; children: [
const DateTypeOptionWidget({ _renderDateFormatButton(
required this.typeOptionContext, typeOption,
required this.popoverMutex, popoverMutex,
super.key, onTypeOptionUpdated,
}); ),
VSpace(GridSize.typeOptionSeparatorHeight),
@override _renderTimeFormatButton(
Widget build(BuildContext context) { typeOption,
return BlocProvider( popoverMutex,
create: (context) => onTypeOptionUpdated,
DateTypeOptionBloc(typeOptionContext: typeOptionContext), ),
child: BlocConsumer<DateTypeOptionBloc, DateTypeOptionState>( ],
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),
];
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( Widget _renderDateFormatButton(
BuildContext context, DateTypeOptionPB typeOption,
DateFormatPB dataFormat, PopoverMutex popoverMutex,
TypeOptionDataCallback onTypeOptionUpdated,
) { ) {
return AppFlowyPopover( return AppFlowyPopover(
mutex: popoverMutex, mutex: popoverMutex,
@ -84,11 +53,11 @@ class DateTypeOptionWidget extends TypeOptionWidget {
constraints: BoxConstraints.loose(const Size(460, 440)), constraints: BoxConstraints.loose(const Size(460, 440)),
popupBuilder: (popoverContext) { popupBuilder: (popoverContext) {
return DateFormatList( return DateFormatList(
selectedFormat: dataFormat, selectedFormat: typeOption.dateFormat,
onSelected: (format) { onSelected: (format) {
context final newTypeOption =
.read<DateTypeOptionBloc>() _updateTypeOption(typeOption: typeOption, dateFormat: format);
.add(DateTypeOptionEvent.didSelectDateFormat(format)); onTypeOptionUpdated(newTypeOption.writeToBuffer());
PopoverContainer.of(popoverContext).close(); PopoverContainer.of(popoverContext).close();
}, },
); );
@ -101,8 +70,9 @@ class DateTypeOptionWidget extends TypeOptionWidget {
} }
Widget _renderTimeFormatButton( Widget _renderTimeFormatButton(
BuildContext context, DateTypeOptionPB typeOption,
TimeFormatPB timeFormat, PopoverMutex popoverMutex,
TypeOptionDataCallback onTypeOptionUpdated,
) { ) {
return AppFlowyPopover( return AppFlowyPopover(
mutex: popoverMutex, mutex: popoverMutex,
@ -112,227 +82,40 @@ class DateTypeOptionWidget extends TypeOptionWidget {
constraints: BoxConstraints.loose(const Size(460, 440)), constraints: BoxConstraints.loose(const Size(460, 440)),
popupBuilder: (BuildContext popoverContext) { popupBuilder: (BuildContext popoverContext) {
return TimeFormatList( return TimeFormatList(
selectedFormat: timeFormat, selectedFormat: typeOption.timeFormat,
onSelected: (format) { onSelected: (format) {
context final newTypeOption =
.read<DateTypeOptionBloc>() _updateTypeOption(typeOption: typeOption, timeFormat: format);
.add(DateTypeOptionEvent.didSelectTimeFormat(format)); onTypeOptionUpdated(newTypeOption.writeToBuffer());
PopoverContainer.of(popoverContext).close(); PopoverContainer.of(popoverContext).close();
}, },
); );
}, },
child: Padding( child: const Padding(
padding: const EdgeInsets.symmetric(horizontal: 12.0), padding: EdgeInsets.symmetric(horizontal: 12.0),
child: TimeFormatButton(timeFormat: timeFormat), child: TimeFormatButton(),
), ),
); );
} }
}
class DateFormatButton extends StatelessWidget { DateTypeOptionPB _parseTypeOptionData(List<int> data) {
final VoidCallback? onTap; return DateTypeOptionDataParser().fromBuffer(data);
final void Function(bool)? onHover; }
const DateFormatButton({
this.onTap,
this.onHover,
super.key,
});
@override DateTypeOptionPB _updateTypeOption({
Widget build(BuildContext context) { required DateTypeOptionPB typeOption,
return SizedBox( DateFormatPB? dateFormat,
height: GridSize.popoverItemHeight, TimeFormatPB? timeFormat,
child: FlowyButton( }) {
text: FlowyText.medium(LocaleKeys.grid_field_dateFormat.tr()), typeOption.freeze();
onTap: onTap, return typeOption.rebuild((typeOption) {
onHover: onHover, if (dateFormat != null) {
rightIcon: const FlowySvg(FlowySvgs.more_s), typeOption.dateFormat = dateFormat;
), }
);
} if (timeFormat != null) {
} typeOption.timeFormat = timeFormat;
}
class TimeFormatButton extends StatelessWidget { });
final TimeFormatPB timeFormat;
final VoidCallback? onTap;
final void Function(bool)? onHover;
const TimeFormatButton({
required this.timeFormat,
this.onTap,
this.onHover,
super.key,
});
@override
Widget build(BuildContext context) {
return SizedBox(
height: GridSize.popoverItemHeight,
child: FlowyButton(
text: FlowyText.medium(LocaleKeys.grid_field_timeFormat.tr()),
onTap: onTap,
onHover: onHover,
rightIcon: const FlowySvg(FlowySvgs.more_s),
),
);
}
}
class DateFormatList extends StatelessWidget {
final DateFormatPB selectedFormat;
final Function(DateFormatPB format) onSelected;
const DateFormatList({
required this.selectedFormat,
required this.onSelected,
super.key,
});
@override
Widget build(BuildContext context) {
final cells = DateFormatPB.values.map((format) {
return DateFormatCell(
dateFormat: format,
onSelected: onSelected,
isSelected: selectedFormat == format,
);
}).toList();
return SizedBox(
width: 180,
child: ListView.separated(
shrinkWrap: true,
separatorBuilder: (context, index) {
return VSpace(GridSize.typeOptionSeparatorHeight);
},
itemCount: cells.length,
itemBuilder: (BuildContext context, int index) {
return cells[index];
},
),
);
}
}
class DateFormatCell extends StatelessWidget {
final bool isSelected;
final DateFormatPB dateFormat;
final Function(DateFormatPB format) onSelected;
const DateFormatCell({
required this.dateFormat,
required this.onSelected,
required this.isSelected,
super.key,
});
@override
Widget build(BuildContext context) {
Widget? checkmark;
if (isSelected) {
checkmark = const FlowySvg(FlowySvgs.check_s);
}
return SizedBox(
height: GridSize.popoverItemHeight,
child: FlowyButton(
text: FlowyText.medium(dateFormat.title()),
rightIcon: checkmark,
onTap: () => onSelected(dateFormat),
),
);
}
}
extension DateFormatExtension on DateFormatPB {
String title() {
switch (this) {
case DateFormatPB.Friendly:
return LocaleKeys.grid_field_dateFormatFriendly.tr();
case DateFormatPB.ISO:
return LocaleKeys.grid_field_dateFormatISO.tr();
case DateFormatPB.Local:
return LocaleKeys.grid_field_dateFormatLocal.tr();
case DateFormatPB.US:
return LocaleKeys.grid_field_dateFormatUS.tr();
case DateFormatPB.DayMonthYear:
return LocaleKeys.grid_field_dateFormatDayMonthYear.tr();
default:
throw UnimplementedError;
}
}
}
class TimeFormatList extends StatelessWidget {
final TimeFormatPB selectedFormat;
final Function(TimeFormatPB format) onSelected;
const TimeFormatList({
required this.selectedFormat,
required this.onSelected,
super.key,
});
@override
Widget build(BuildContext context) {
final cells = TimeFormatPB.values.map((format) {
return TimeFormatCell(
isSelected: format == selectedFormat,
timeFormat: format,
onSelected: onSelected,
);
}).toList();
return SizedBox(
width: 120,
child: ListView.separated(
shrinkWrap: true,
separatorBuilder: (context, index) {
return VSpace(GridSize.typeOptionSeparatorHeight);
},
itemCount: cells.length,
itemBuilder: (BuildContext context, int index) {
return cells[index];
},
),
);
}
}
class TimeFormatCell extends StatelessWidget {
final TimeFormatPB timeFormat;
final bool isSelected;
final Function(TimeFormatPB format) onSelected;
const TimeFormatCell({
required this.timeFormat,
required this.onSelected,
required this.isSelected,
super.key,
});
@override
Widget build(BuildContext context) {
Widget? checkmark;
if (isSelected) {
checkmark = const FlowySvg(FlowySvgs.check_s);
}
return SizedBox(
height: GridSize.popoverItemHeight,
child: FlowyButton(
text: FlowyText.medium(timeFormat.title()),
rightIcon: checkmark,
onTap: () => onSelected(timeFormat),
),
);
}
}
extension TimeFormatExtension on TimeFormatPB {
String title() {
switch (this) {
case TimeFormatPB.TwelveHour:
return LocaleKeys.grid_field_timeFormatTwelveHour.tr();
case TimeFormatPB.TwentyFourHour:
return LocaleKeys.grid_field_timeFormatTwentyFourHour.tr();
default:
throw UnimplementedError;
}
} }
} }

View File

@ -0,0 +1,254 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/layout/sizes.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/protobuf.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
class DateFormatButton extends StatelessWidget {
const DateFormatButton({
super.key,
this.onTap,
this.onHover,
});
final VoidCallback? onTap;
final void Function(bool)? onHover;
@override
Widget build(BuildContext context) {
return SizedBox(
height: GridSize.popoverItemHeight,
child: FlowyButton(
text: FlowyText.medium(LocaleKeys.grid_field_dateFormat.tr()),
onTap: onTap,
onHover: onHover,
rightIcon: const FlowySvg(FlowySvgs.more_s),
),
);
}
}
class TimeFormatButton extends StatelessWidget {
const TimeFormatButton({
super.key,
this.onTap,
this.onHover,
});
final VoidCallback? onTap;
final void Function(bool)? onHover;
@override
Widget build(BuildContext context) {
return SizedBox(
height: GridSize.popoverItemHeight,
child: FlowyButton(
text: FlowyText.medium(LocaleKeys.grid_field_timeFormat.tr()),
onTap: onTap,
onHover: onHover,
rightIcon: const FlowySvg(FlowySvgs.more_s),
),
);
}
}
class DateFormatList extends StatelessWidget {
final DateFormatPB selectedFormat;
final Function(DateFormatPB format) onSelected;
const DateFormatList({
required this.selectedFormat,
required this.onSelected,
super.key,
});
@override
Widget build(BuildContext context) {
final cells = DateFormatPB.values.map((format) {
return DateFormatCell(
dateFormat: format,
onSelected: onSelected,
isSelected: selectedFormat == format,
);
}).toList();
return SizedBox(
width: 180,
child: ListView.separated(
shrinkWrap: true,
separatorBuilder: (context, index) {
return VSpace(GridSize.typeOptionSeparatorHeight);
},
itemCount: cells.length,
itemBuilder: (BuildContext context, int index) {
return cells[index];
},
),
);
}
}
class DateFormatCell extends StatelessWidget {
final bool isSelected;
final DateFormatPB dateFormat;
final Function(DateFormatPB format) onSelected;
const DateFormatCell({
required this.dateFormat,
required this.onSelected,
required this.isSelected,
super.key,
});
@override
Widget build(BuildContext context) {
Widget? checkmark;
if (isSelected) {
checkmark = const FlowySvg(FlowySvgs.check_s);
}
return SizedBox(
height: GridSize.popoverItemHeight,
child: FlowyButton(
text: FlowyText.medium(dateFormat.title()),
rightIcon: checkmark,
onTap: () => onSelected(dateFormat),
),
);
}
}
extension DateFormatExtension on DateFormatPB {
String title() {
switch (this) {
case DateFormatPB.Friendly:
return LocaleKeys.grid_field_dateFormatFriendly.tr();
case DateFormatPB.ISO:
return LocaleKeys.grid_field_dateFormatISO.tr();
case DateFormatPB.Local:
return LocaleKeys.grid_field_dateFormatLocal.tr();
case DateFormatPB.US:
return LocaleKeys.grid_field_dateFormatUS.tr();
case DateFormatPB.DayMonthYear:
return LocaleKeys.grid_field_dateFormatDayMonthYear.tr();
default:
throw UnimplementedError;
}
}
}
class TimeFormatList extends StatelessWidget {
final TimeFormatPB selectedFormat;
final Function(TimeFormatPB format) onSelected;
const TimeFormatList({
required this.selectedFormat,
required this.onSelected,
super.key,
});
@override
Widget build(BuildContext context) {
final cells = TimeFormatPB.values.map((format) {
return TimeFormatCell(
isSelected: format == selectedFormat,
timeFormat: format,
onSelected: onSelected,
);
}).toList();
return SizedBox(
width: 120,
child: ListView.separated(
shrinkWrap: true,
separatorBuilder: (context, index) {
return VSpace(GridSize.typeOptionSeparatorHeight);
},
itemCount: cells.length,
itemBuilder: (BuildContext context, int index) {
return cells[index];
},
),
);
}
}
class TimeFormatCell extends StatelessWidget {
final TimeFormatPB timeFormat;
final bool isSelected;
final Function(TimeFormatPB format) onSelected;
const TimeFormatCell({
required this.timeFormat,
required this.onSelected,
required this.isSelected,
super.key,
});
@override
Widget build(BuildContext context) {
Widget? checkmark;
if (isSelected) {
checkmark = const FlowySvg(FlowySvgs.check_s);
}
return SizedBox(
height: GridSize.popoverItemHeight,
child: FlowyButton(
text: FlowyText.medium(timeFormat.title()),
rightIcon: checkmark,
onTap: () => onSelected(timeFormat),
),
);
}
}
extension TimeFormatExtension on TimeFormatPB {
String title() {
switch (this) {
case TimeFormatPB.TwelveHour:
return LocaleKeys.grid_field_timeFormatTwelveHour.tr();
case TimeFormatPB.TwentyFourHour:
return LocaleKeys.grid_field_timeFormatTwentyFourHour.tr();
default:
throw UnimplementedError;
}
}
}
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,
),
],
),
),
);
}
}

View File

@ -1,51 +1,38 @@
import 'package:appflowy/plugins/database_view/application/field/type_option/multi_select_type_option.dart'; import 'package:appflowy/plugins/database_view/application/field/type_option/multi_select_type_option.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_data_parser.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:appflowy_popover/appflowy_popover.dart';
import '../field_type_option_editor.dart';
import 'builder.dart'; import 'builder.dart';
import 'select_option.dart'; import 'select/select_option.dart';
class MultiSelectTypeOptionWidgetBuilder extends TypeOptionWidgetBuilder { class MultiSelectTypeOptionEditorFactory implements TypeOptionEditorFactory {
final MultiSelectTypeOptionWidget _widget; const MultiSelectTypeOptionEditorFactory();
MultiSelectTypeOptionWidgetBuilder(
MultiSelectTypeOptionContext typeOptionContext,
PopoverMutex popoverMutex,
) : _widget = MultiSelectTypeOptionWidget(
selectOptionAction: MultiSelectAction(
fieldId: typeOptionContext.fieldId,
viewId: typeOptionContext.viewId,
typeOptionContext: typeOptionContext,
),
popoverMutex: popoverMutex,
);
@override @override
Widget? build(BuildContext context) => _widget; Widget? build({
} required BuildContext context,
required String viewId,
required FieldPB field,
required PopoverMutex popoverMutex,
required TypeOptionDataCallback onTypeOptionUpdated,
}) {
final typeOption = _parseTypeOptionData(field.typeOptionData);
class MultiSelectTypeOptionWidget extends TypeOptionWidget {
final MultiSelectAction selectOptionAction;
final PopoverMutex? popoverMutex;
const MultiSelectTypeOptionWidget({
super.key,
required this.selectOptionAction,
this.popoverMutex,
});
@override
Widget build(BuildContext context) {
return SelectOptionTypeOptionWidget( return SelectOptionTypeOptionWidget(
options: selectOptionAction.typeOption.options, options: typeOption.options,
beginEdit: () { beginEdit: () => PopoverContainer.of(context).closeAll(),
PopoverContainer.of(context).closeAll();
},
popoverMutex: popoverMutex, popoverMutex: popoverMutex,
typeOptionAction: selectOptionAction, typeOptionAction: MultiSelectAction(
// key: ValueKey(state.typeOption.hashCode), viewId: viewId,
fieldId: field.id,
onTypeOptionUpdated: onTypeOptionUpdated,
),
); );
} }
MultiSelectTypeOptionPB _parseTypeOptionData(List<int> data) {
return MultiSelectTypeOptionDataParser().fromBuffer(data);
}
} }

View File

@ -1,127 +1,106 @@
import 'package:appflowy/generated/flowy_svgs.g.dart'; 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/field/type_option/number_bloc.dart';
import 'package:appflowy/plugins/database_view/application/field/type_option/number_format_bloc.dart'; import 'package:appflowy/plugins/database_view/application/field/type_option/number_format_bloc.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_data_parser.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/number_entities.pbenum.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:protobuf/protobuf.dart';
import '../../../layout/sizes.dart'; import '../../../layout/sizes.dart';
import '../../common/type_option_separator.dart'; import '../../common/type_option_separator.dart';
import '../field_type_option_editor.dart';
import 'builder.dart'; import 'builder.dart';
class NumberTypeOptionWidgetBuilder extends TypeOptionWidgetBuilder { class NumberTypeOptionEditorFactory implements TypeOptionEditorFactory {
final NumberTypeOptionWidget _widget; const NumberTypeOptionEditorFactory();
NumberTypeOptionWidgetBuilder(
NumberTypeOptionContext typeOptionContext,
PopoverMutex popoverMutex,
) : _widget = NumberTypeOptionWidget(
typeOptionContext: typeOptionContext,
popoverMutex: popoverMutex,
);
@override @override
Widget? build(BuildContext context) { Widget? build({
return Column( required BuildContext context,
children: [ required String viewId,
VSpace(GridSize.typeOptionSeparatorHeight), required FieldPB field,
const TypeOptionSeparator(), required PopoverMutex popoverMutex,
_widget, required TypeOptionDataCallback onTypeOptionUpdated,
], }) {
final typeOption = _parseTypeOptionData(field.typeOptionData);
final selectNumUnitButton = SizedBox(
height: GridSize.popoverItemHeight,
child: FlowyButton(
rightIcon: const FlowySvg(FlowySvgs.more_s),
text: FlowyText.medium(
typeOption.format.title(),
),
),
); );
}
}
class NumberTypeOptionWidget extends TypeOptionWidget { final numFormatTitle = Container(
final NumberTypeOptionContext typeOptionContext; padding: const EdgeInsets.only(left: 6),
final PopoverMutex popoverMutex; height: GridSize.popoverItemHeight,
alignment: Alignment.centerLeft,
child: FlowyText.regular(
LocaleKeys.grid_field_numberFormat.tr(),
color: Theme.of(context).hintColor,
fontSize: 11,
),
);
const NumberTypeOptionWidget({ return Padding(
super.key, padding: const EdgeInsets.symmetric(horizontal: 8),
required this.typeOptionContext, child: Column(
required this.popoverMutex, crossAxisAlignment: CrossAxisAlignment.start,
}); children: [
numFormatTitle,
@override AppFlowyPopover(
Widget build(BuildContext context) { mutex: popoverMutex,
return BlocProvider( triggerActions:
create: (context) => PopoverTriggerFlags.hover | PopoverTriggerFlags.click,
NumberTypeOptionBloc(typeOptionContext: typeOptionContext), offset: const Offset(16, 0),
child: BlocConsumer<NumberTypeOptionBloc, NumberTypeOptionState>( constraints: BoxConstraints.loose(const Size(460, 440)),
listener: (context, state) => margin: EdgeInsets.zero,
typeOptionContext.typeOption = state.typeOption, child: selectNumUnitButton,
builder: (context, state) { popupBuilder: (BuildContext popoverContext) {
final selectNumUnitButton = SizedBox( return NumberFormatList(
height: GridSize.popoverItemHeight, selectedFormat: typeOption.format,
child: FlowyButton( onSelected: (format) {
rightIcon: const FlowySvg(FlowySvgs.more_s), final newTypeOption = _updateNumberFormat(typeOption, format);
text: FlowyText.regular( onTypeOptionUpdated(newTypeOption.writeToBuffer());
state.typeOption.format.title(), PopoverContainer.of(popoverContext).close();
), },
), );
); },
),
final numFormatTitle = Container( ],
padding: const EdgeInsets.only(left: 6),
height: GridSize.popoverItemHeight,
alignment: Alignment.centerLeft,
child: FlowyText.regular(
LocaleKeys.grid_field_numberFormat.tr(),
color: Theme.of(context).hintColor,
fontSize: 11,
),
);
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
numFormatTitle,
AppFlowyPopover(
mutex: popoverMutex,
triggerActions:
PopoverTriggerFlags.hover | PopoverTriggerFlags.click,
offset: const Offset(16, 0),
constraints: BoxConstraints.loose(const Size(460, 440)),
margin: EdgeInsets.zero,
child: selectNumUnitButton,
popupBuilder: (BuildContext popoverContext) {
return NumberFormatList(
onSelected: (format) {
context
.read<NumberTypeOptionBloc>()
.add(NumberTypeOptionEvent.didSelectFormat(format));
PopoverContainer.of(popoverContext).close();
},
selectedFormat: state.typeOption.format,
);
},
),
],
),
);
},
), ),
); );
} }
NumberTypeOptionPB _parseTypeOptionData(List<int> data) {
return NumberTypeOptionDataParser().fromBuffer(data);
}
NumberTypeOptionPB _updateNumberFormat(
NumberTypeOptionPB typeOption,
NumberFormatPB format,
) {
typeOption.freeze();
return typeOption.rebuild((typeOption) => typeOption.format = format);
}
} }
typedef SelectNumberFormatCallback = Function(NumberFormatPB format); typedef SelectNumberFormatCallback = void Function(NumberFormatPB format);
class NumberFormatList extends StatelessWidget { class NumberFormatList extends StatelessWidget {
final SelectNumberFormatCallback onSelected; final SelectNumberFormatCallback onSelected;
final NumberFormatPB selectedFormat; final NumberFormatPB selectedFormat;
const NumberFormatList({ const NumberFormatList({
super.key,
required this.selectedFormat, required this.selectedFormat,
required this.onSelected, required this.onSelected,
super.key,
}); });
@override @override
@ -171,7 +150,7 @@ class NumberFormatList extends StatelessWidget {
class NumberFormatCell extends StatelessWidget { class NumberFormatCell extends StatelessWidget {
final NumberFormatPB format; final NumberFormatPB format;
final bool isSelected; final bool isSelected;
final Function(NumberFormatPB format) onSelected; final SelectNumberFormatCallback onSelected;
const NumberFormatCell({ const NumberFormatCell({
required this.isSelected, required this.isSelected,
required this.format, required this.format,
@ -181,10 +160,7 @@ class NumberFormatCell extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
Widget? checkmark; final checkmark = isSelected ? const FlowySvg(FlowySvgs.check_s) : null;
if (isSelected) {
checkmark = const FlowySvg(FlowySvgs.check_s);
}
return SizedBox( return SizedBox(
height: GridSize.popoverItemHeight, height: GridSize.popoverItemHeight,

View File

@ -1,10 +1,18 @@
import 'package:appflowy/plugins/database_view/application/field/type_option/type_option_context.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'builder.dart'; import 'builder.dart';
class RichTextTypeOptionWidgetBuilder extends TypeOptionWidgetBuilder { class RichTextTypeOptionEditorFactory implements TypeOptionEditorFactory {
RichTextTypeOptionWidgetBuilder(RichTextTypeOptionContext typeOptionContext); const RichTextTypeOptionEditorFactory();
@override @override
Widget? build(BuildContext context) => null; Widget? build({
required BuildContext context,
required String viewId,
required FieldPB field,
required PopoverMutex popoverMutex,
required TypeOptionDataCallback onTypeOptionUpdated,
}) =>
null;
} }

View File

@ -1,17 +1,15 @@
import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/plugins/database_view/application/field/type_option/select_option_type_option_bloc.dart'; import 'package:appflowy/plugins/database_view/application/field/type_option/select_option_type_option_bloc.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/layout/sizes.dart';
import 'package:appflowy/plugins/database_view/widgets/row/cells/select_option_cell/extension.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_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/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart';
import '../../../layout/sizes.dart';
import '../../../../../widgets/row/cells/select_option_cell/extension.dart';
import '../../common/type_option_separator.dart';
import 'select_option_editor.dart'; import 'select_option_editor.dart';
class SelectOptionTypeOptionWidget extends StatelessWidget { class SelectOptionTypeOptionWidget extends StatelessWidget {
@ -39,7 +37,6 @@ class SelectOptionTypeOptionWidget extends StatelessWidget {
BlocBuilder<SelectOptionTypeOptionBloc, SelectOptionTypeOptionState>( BlocBuilder<SelectOptionTypeOptionBloc, SelectOptionTypeOptionState>(
builder: (context, state) { builder: (context, state) {
final List<Widget> children = [ final List<Widget> children = [
const TypeOptionSeparator(spacing: 8),
const _OptionTitle(), const _OptionTitle(),
const VSpace(4), const VSpace(4),
if (state.isEditingOption) ...[ if (state.isEditingOption) ...[
@ -127,13 +124,7 @@ class _OptionCell extends StatefulWidget {
} }
class _OptionCellState extends State<_OptionCell> { class _OptionCellState extends State<_OptionCell> {
late PopoverController _popoverController; final PopoverController _popoverController = PopoverController();
@override
void initState() {
_popoverController = PopoverController();
super.initState();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -230,18 +221,18 @@ class _CreateOptionTextFieldState extends State<CreateOptionTextField> {
@override @override
void initState() { void initState() {
_focusNode = FocusNode(); super.initState();
_focusNode.addListener(() { _focusNode = FocusNode()
if (_focusNode.hasFocus) { ..addListener(() {
widget.popoverMutex?.close(); if (_focusNode.hasFocus) {
} widget.popoverMutex?.close();
}); }
});
widget.popoverMutex?.listenOnPopoverChanged(() { widget.popoverMutex?.listenOnPopoverChanged(() {
if (_focusNode.hasFocus) { if (_focusNode.hasFocus) {
_focusNode.unfocus(); _focusNode.unfocus();
} }
}); });
super.initState();
} }
@override @override

View File

@ -13,8 +13,8 @@ import 'package:flowy_infra_ui/widget/spacing.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../layout/sizes.dart'; import '../../../../layout/sizes.dart';
import '../../common/type_option_separator.dart'; import '../../../common/type_option_separator.dart';
class SelectOptionTypeOptionEditor extends StatelessWidget { class SelectOptionTypeOptionEditor extends StatelessWidget {
final SelectOptionPB option; final SelectOptionPB option;

View File

@ -1,49 +1,38 @@
import 'package:appflowy/plugins/database_view/application/field/type_option/single_select_type_option.dart'; import 'package:appflowy/plugins/database_view/application/field/type_option/single_select_type_option.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_data_parser.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../field_type_option_editor.dart';
import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:appflowy_popover/appflowy_popover.dart';
import 'builder.dart'; import 'builder.dart';
import 'select_option.dart'; import 'select/select_option.dart';
class SingleSelectTypeOptionWidgetBuilder extends TypeOptionWidgetBuilder { class SingleSelectTypeOptionEditorFactory implements TypeOptionEditorFactory {
final SingleSelectTypeOptionWidget _widget; const SingleSelectTypeOptionEditorFactory();
SingleSelectTypeOptionWidgetBuilder(
SingleSelectTypeOptionContext singleSelectTypeOption,
PopoverMutex popoverMutex,
) : _widget = SingleSelectTypeOptionWidget(
selectOptionAction: SingleSelectAction(
fieldId: singleSelectTypeOption.fieldId,
viewId: singleSelectTypeOption.viewId,
typeOptionContext: singleSelectTypeOption,
),
popoverMutex: popoverMutex,
);
@override @override
Widget? build(BuildContext context) => _widget; Widget? build({
} required BuildContext context,
required String viewId,
required FieldPB field,
required PopoverMutex popoverMutex,
required TypeOptionDataCallback onTypeOptionUpdated,
}) {
final typeOption = _parseTypeOptionData(field.typeOptionData);
class SingleSelectTypeOptionWidget extends TypeOptionWidget {
final SingleSelectAction selectOptionAction;
final PopoverMutex? popoverMutex;
const SingleSelectTypeOptionWidget({
super.key,
required this.selectOptionAction,
this.popoverMutex,
});
@override
Widget build(BuildContext context) {
return SelectOptionTypeOptionWidget( return SelectOptionTypeOptionWidget(
options: selectOptionAction.typeOption.options, options: typeOption.options,
beginEdit: () { beginEdit: () => PopoverContainer.of(context).closeAll(),
PopoverContainer.of(context).closeAll();
},
popoverMutex: popoverMutex, popoverMutex: popoverMutex,
typeOptionAction: selectOptionAction, typeOptionAction: SingleSelectAction(
viewId: viewId,
fieldId: field.id,
onTypeOptionUpdated: onTypeOptionUpdated,
),
); );
} }
SingleSelectTypeOptionPB _parseTypeOptionData(List<int> data) {
return SingleSelectTypeOptionDataParser().fromBuffer(data);
}
} }

View File

@ -1,88 +1,54 @@
import 'package:flutter/material.dart'; import 'package:appflowy/plugins/database_view/application/field/type_option/type_option_data_parser.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/layout/sizes.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/common/type_option_separator.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/field_type_option_editor.dart';
import 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/include_time_button.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/date_entities.pb.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_bloc/flutter_bloc.dart'; import 'package:flutter/material.dart';
import 'package:protobuf/protobuf.dart';
import 'builder.dart'; import 'builder.dart';
import 'date.dart'; import 'date/date_time_format.dart';
class TimestampTypeOptionWidgetBuilder extends TypeOptionWidgetBuilder { class TimestampTypeOptionEditorFactory implements TypeOptionEditorFactory {
TimestampTypeOptionWidgetBuilder( const TimestampTypeOptionEditorFactory();
TimestampTypeOptionContext typeOptionContext,
PopoverMutex popoverMutex,
) : _widget = TimestampTypeOptionWidget(
typeOptionContext: typeOptionContext,
popoverMutex: popoverMutex,
);
final TimestampTypeOptionWidget _widget;
@override @override
Widget? build(BuildContext context) => _widget; Widget? build({
} required BuildContext context,
required String viewId,
required FieldPB field,
required PopoverMutex popoverMutex,
required TypeOptionDataCallback onTypeOptionUpdated,
}) {
final typeOption = _parseTypeOptionData(field.typeOptionData);
class TimestampTypeOptionWidget extends TypeOptionWidget { return SeparatedColumn(
const TimestampTypeOptionWidget({ mainAxisSize: MainAxisSize.min,
super.key, separatorBuilder: () => VSpace(GridSize.typeOptionSeparatorHeight),
required this.typeOptionContext, children: [
required this.popoverMutex, _renderDateFormatButton(typeOption, popoverMutex, onTypeOptionUpdated),
}); _renderTimeFormatButton(typeOption, popoverMutex, onTypeOptionUpdated),
Padding(
final TimestampTypeOptionContext typeOptionContext; padding: const EdgeInsets.symmetric(horizontal: 12.0),
final PopoverMutex popoverMutex; child: IncludeTimeButton(
onChanged: (value) {
@override final newTypeOption = _updateTypeOption(
Widget build(BuildContext context) { typeOption: typeOption,
return BlocProvider( includeTime: !value,
create: (context) => );
TimestampTypeOptionBloc(typeOptionContext: typeOptionContext), onTypeOptionUpdated(newTypeOption.writeToBuffer());
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.shrink();
} else {
return VSpace(GridSize.typeOptionSeparatorHeight);
}
}, },
itemCount: children.length, value: typeOption.includeTime,
itemBuilder: (BuildContext context, int index) => children[index], ),
); ),
}, ],
),
); );
} }
Widget _renderDateFormatButton( Widget _renderDateFormatButton(
BuildContext context, TimestampTypeOptionPB typeOption,
DateFormatPB dataFormat, PopoverMutex popoverMutex,
TypeOptionDataCallback onTypeOptionUpdated,
) { ) {
return AppFlowyPopover( return AppFlowyPopover(
mutex: popoverMutex, mutex: popoverMutex,
@ -90,15 +56,17 @@ class TimestampTypeOptionWidget extends TypeOptionWidget {
triggerActions: PopoverTriggerFlags.hover | PopoverTriggerFlags.click, triggerActions: PopoverTriggerFlags.hover | PopoverTriggerFlags.click,
offset: const Offset(8, 0), offset: const Offset(8, 0),
constraints: BoxConstraints.loose(const Size(460, 440)), constraints: BoxConstraints.loose(const Size(460, 440)),
popupBuilder: (popoverContext) => DateFormatList( popupBuilder: (popoverContext) {
selectedFormat: dataFormat, return DateFormatList(
onSelected: (format) { selectedFormat: typeOption.dateFormat,
context onSelected: (format) {
.read<TimestampTypeOptionBloc>() final newTypeOption =
.add(TimestampTypeOptionEvent.didSelectDateFormat(format)); _updateTypeOption(typeOption: typeOption, dateFormat: format);
PopoverContainer.of(popoverContext).close(); onTypeOptionUpdated(newTypeOption.writeToBuffer());
}, PopoverContainer.of(popoverContext).close();
), },
);
},
child: const Padding( child: const Padding(
padding: EdgeInsets.symmetric(horizontal: 12.0), padding: EdgeInsets.symmetric(horizontal: 12.0),
child: DateFormatButton(), child: DateFormatButton(),
@ -107,8 +75,9 @@ class TimestampTypeOptionWidget extends TypeOptionWidget {
} }
Widget _renderTimeFormatButton( Widget _renderTimeFormatButton(
BuildContext context, TimestampTypeOptionPB typeOption,
TimeFormatPB timeFormat, PopoverMutex popoverMutex,
TypeOptionDataCallback onTypeOptionUpdated,
) { ) {
return AppFlowyPopover( return AppFlowyPopover(
mutex: popoverMutex, mutex: popoverMutex,
@ -116,19 +85,47 @@ class TimestampTypeOptionWidget extends TypeOptionWidget {
triggerActions: PopoverTriggerFlags.hover | PopoverTriggerFlags.click, triggerActions: PopoverTriggerFlags.hover | PopoverTriggerFlags.click,
offset: const Offset(8, 0), offset: const Offset(8, 0),
constraints: BoxConstraints.loose(const Size(460, 440)), constraints: BoxConstraints.loose(const Size(460, 440)),
popupBuilder: (BuildContext popoverContext) => TimeFormatList( popupBuilder: (BuildContext popoverContext) {
selectedFormat: timeFormat, return TimeFormatList(
onSelected: (format) { selectedFormat: typeOption.timeFormat,
context onSelected: (format) {
.read<TimestampTypeOptionBloc>() final newTypeOption =
.add(TimestampTypeOptionEvent.didSelectTimeFormat(format)); _updateTypeOption(typeOption: typeOption, timeFormat: format);
PopoverContainer.of(popoverContext).close(); onTypeOptionUpdated(newTypeOption.writeToBuffer());
}, PopoverContainer.of(popoverContext).close();
), },
child: Padding( );
padding: const EdgeInsets.symmetric(horizontal: 12.0), },
child: TimeFormatButton(timeFormat: timeFormat), child: const Padding(
padding: EdgeInsets.symmetric(horizontal: 12.0),
child: TimeFormatButton(),
), ),
); );
} }
TimestampTypeOptionPB _parseTypeOptionData(List<int> data) {
return TimestampTypeOptionDataParser().fromBuffer(data);
}
TimestampTypeOptionPB _updateTypeOption({
required TimestampTypeOptionPB typeOption,
DateFormatPB? dateFormat,
TimeFormatPB? timeFormat,
bool? includeTime,
}) {
typeOption.freeze();
return typeOption.rebuild((typeOption) {
if (dateFormat != null) {
typeOption.dateFormat = dateFormat;
}
if (timeFormat != null) {
typeOption.timeFormat = timeFormat;
}
if (includeTime != null) {
typeOption.includeTime = includeTime;
}
});
}
} }

View File

@ -1,10 +1,18 @@
import 'package:appflowy/plugins/database_view/application/field/type_option/type_option_context.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'builder.dart'; import 'builder.dart';
class URLTypeOptionWidgetBuilder extends TypeOptionWidgetBuilder { class URLTypeOptionEditorFactory implements TypeOptionEditorFactory {
URLTypeOptionWidgetBuilder(URLTypeOptionContext typeOptionContext); const URLTypeOptionEditorFactory();
@override @override
Widget? build(BuildContext context) => null; Widget? build({
required BuildContext context,
required String viewId,
required FieldPB field,
required PopoverMutex popoverMutex,
required TypeOptionDataCallback onTypeOptionUpdated,
}) =>
null;
} }

View File

@ -47,17 +47,17 @@ class GridDateCell extends GridCellWidget {
} }
class _DateCellState extends GridCellState<GridDateCell> { class _DateCellState extends GridCellState<GridDateCell> {
late PopoverController _popover; final PopoverController _popover = PopoverController();
late final DateCellController _cellController;
late DateCellBloc _cellBloc; late DateCellBloc _cellBloc;
@override @override
void initState() { void initState() {
_popover = PopoverController();
final cellController =
widget.cellControllerBuilder.build() as DateCellController;
_cellBloc = DateCellBloc(cellController: cellController)
..add(const DateCellEvent.initial());
super.initState(); super.initState();
_cellController =
widget.cellControllerBuilder.build() as DateCellController;
_cellBloc = DateCellBloc(cellController: _cellController)
..add(const DateCellEvent.initial());
} }
@override @override
@ -93,8 +93,7 @@ class _DateCellState extends GridCellState<GridDateCell> {
), ),
popupBuilder: (BuildContext popoverContent) { popupBuilder: (BuildContext popoverContent) {
return DateCellEditor( return DateCellEditor(
cellController: widget.cellControllerBuilder.build() cellController: _cellController,
as DateCellController,
onDismissed: () => onDismissed: () =>
widget.cellContainerNotifier.isFocus = false, widget.cellContainerNotifier.isFocus = false,
); );
@ -111,8 +110,7 @@ class _DateCellState extends GridCellState<GridDateCell> {
padding: EdgeInsets.zero, padding: EdgeInsets.zero,
builder: (context) { builder: (context) {
return MobileDateCellEditScreen( return MobileDateCellEditScreen(
controller: widget.cellControllerBuilder.build() controller: _cellController,
as DateCellController,
showAsFullScreen: false, showAsFullScreen: false,
); );
}, },
@ -169,8 +167,7 @@ class _DateCellState extends GridCellState<GridDateCell> {
Theme.of(context).colorScheme.secondaryContainer, Theme.of(context).colorScheme.secondaryContainer,
builder: (context) { builder: (context) {
return MobileDateCellEditScreen( return MobileDateCellEditScreen(
controller: widget.cellControllerBuilder.build() controller: _cellController,
as DateCellController,
showAsFullScreen: false, showAsFullScreen: false,
); );
}, },
@ -186,6 +183,7 @@ class _DateCellState extends GridCellState<GridDateCell> {
@override @override
Future<void> dispose() async { Future<void> dispose() async {
_cellBloc.close(); _cellBloc.close();
_cellController.dispose();
super.dispose(); super.dispose();
} }

View File

@ -37,7 +37,6 @@ class DateCellBloc extends Bloc<DateCellEvent, DateCellState> {
cellController.removeListener(_onCellChangedFn!); cellController.removeListener(_onCellChangedFn!);
_onCellChangedFn = null; _onCellChangedFn = null;
} }
await cellController.dispose();
return super.close(); return super.close();
} }

View File

@ -4,6 +4,7 @@ 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/cell/date_cell_service.dart'; import 'package:appflowy/plugins/database_view/application/cell/date_cell_service.dart';
import 'package:appflowy/plugins/database_view/application/field/field_service.dart'; import 'package:appflowy/plugins/database_view/application/field/field_service.dart';
import 'package:appflowy/plugins/database_view/application/field/type_option/type_option_data_parser.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/code.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-error/code.pb.dart';
@ -15,25 +16,23 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:protobuf/protobuf.dart'; import 'package:protobuf/protobuf.dart';
part 'date_cal_bloc.freezed.dart'; part 'date_cell_editor_bloc.freezed.dart';
class DateCellCalendarBloc class DateCellEditorBloc
extends Bloc<DateCellCalendarEvent, DateCellCalendarState> { extends Bloc<DateCellEditorEvent, DateCellEditorState> {
final DateCellBackendService _dateCellBackendService; final DateCellBackendService _dateCellBackendService;
final DateCellController cellController; final DateCellController cellController;
void Function()? _onCellChangedFn; void Function()? _onCellChangedFn;
DateCellCalendarBloc({ DateCellEditorBloc({
required DateTypeOptionPB dateTypeOptionPB,
required DateCellDataPB? cellData,
required this.cellController, required this.cellController,
}) : _dateCellBackendService = DateCellBackendService( }) : _dateCellBackendService = DateCellBackendService(
viewId: cellController.viewId, viewId: cellController.viewId,
fieldId: cellController.fieldId, fieldId: cellController.fieldId,
rowId: cellController.rowId, rowId: cellController.rowId,
), ),
super(DateCellCalendarState.initial(dateTypeOptionPB, cellData)) { super(DateCellEditorState.initial(cellController)) {
on<DateCellCalendarEvent>( on<DateCellEditorEvent>(
(event, emit) async { (event, emit) async {
await event.when( await event.when(
initial: () async => _startListening(), initial: () async => _startListening(),
@ -222,7 +221,7 @@ class DateCellCalendarBloc
if (!isClosed && if (!isClosed &&
(state.parseEndTimeError != null || state.parseTimeError != null)) { (state.parseEndTimeError != null || state.parseTimeError != null)) {
add( add(
const DateCellCalendarEvent.didReceiveTimeFormatError(null, null), const DateCellEditorEvent.didReceiveTimeFormatError(null, null),
); );
} }
}, },
@ -237,7 +236,7 @@ class DateCellCalendarBloc
? (timeFormatPrompt(err), null) ? (timeFormatPrompt(err), null)
: (null, timeFormatPrompt(err)); : (null, timeFormatPrompt(err));
add( add(
DateCellCalendarEvent.didReceiveTimeFormatError( DateCellEditorEvent.didReceiveTimeFormatError(
startError, startError,
endError, endError,
), ),
@ -259,7 +258,7 @@ class DateCellCalendarBloc
} }
add( add(
const DateCellCalendarEvent.didReceiveTimeFormatError(null, null), const DateCellEditorEvent.didReceiveTimeFormatError(null, null),
); );
}, },
(err) => Log.error(err), (err) => Log.error(err),
@ -300,7 +299,6 @@ class DateCellCalendarBloc
cellController.removeListener(_onCellChangedFn!); cellController.removeListener(_onCellChangedFn!);
_onCellChangedFn = null; _onCellChangedFn = null;
} }
await cellController.dispose();
return super.close(); return super.close();
} }
@ -308,14 +306,14 @@ class DateCellCalendarBloc
_onCellChangedFn = cellController.startListening( _onCellChangedFn = cellController.startListening(
onCellChanged: ((cell) { onCellChanged: ((cell) {
if (!isClosed) { if (!isClosed) {
add(DateCellCalendarEvent.didReceiveCellUpdate(cell)); add(DateCellEditorEvent.didReceiveCellUpdate(cell));
} }
}), }),
); );
} }
Future<void>? _updateTypeOption( Future<void>? _updateTypeOption(
Emitter<DateCellCalendarState> emit, { Emitter<DateCellEditorState> emit, {
DateFormatPB? dateFormat, DateFormatPB? dateFormat,
TimeFormatPB? timeFormat, TimeFormatPB? timeFormat,
}) async { }) async {
@ -349,49 +347,49 @@ class DateCellCalendarBloc
} }
@freezed @freezed
class DateCellCalendarEvent with _$DateCellCalendarEvent { class DateCellEditorEvent with _$DateCellEditorEvent {
// initial event // initial event
const factory DateCellCalendarEvent.initial() = _Initial; const factory DateCellEditorEvent.initial() = _Initial;
// notification that cell is updated in the backend // notification that cell is updated in the backend
const factory DateCellCalendarEvent.didReceiveCellUpdate( const factory DateCellEditorEvent.didReceiveCellUpdate(
DateCellDataPB? data, DateCellDataPB? data,
) = _DidReceiveCellUpdate; ) = _DidReceiveCellUpdate;
const factory DateCellCalendarEvent.didReceiveTimeFormatError( const factory DateCellEditorEvent.didReceiveTimeFormatError(
String? parseTimeError, String? parseTimeError,
String? parseEndTimeError, String? parseEndTimeError,
) = _DidReceiveTimeFormatError; ) = _DidReceiveTimeFormatError;
// date cell data is modified // date cell data is modified
const factory DateCellCalendarEvent.selectDay(DateTime day) = _SelectDay; const factory DateCellEditorEvent.selectDay(DateTime day) = _SelectDay;
const factory DateCellCalendarEvent.selectDateRange( const factory DateCellEditorEvent.selectDateRange(
DateTime? start, DateTime? start,
DateTime? end, DateTime? end,
) = _SelectDateRange; ) = _SelectDateRange;
const factory DateCellCalendarEvent.setStartDay( const factory DateCellEditorEvent.setStartDay(
DateTime startDay, DateTime startDay,
) = _SetStartDay; ) = _SetStartDay;
const factory DateCellCalendarEvent.setEndDay( const factory DateCellEditorEvent.setEndDay(
DateTime endDay, DateTime endDay,
) = _SetEndDay; ) = _SetEndDay;
const factory DateCellCalendarEvent.setTime(String time) = _Time; const factory DateCellEditorEvent.setTime(String time) = _Time;
const factory DateCellCalendarEvent.setEndTime(String endTime) = _EndTime; const factory DateCellEditorEvent.setEndTime(String endTime) = _EndTime;
const factory DateCellCalendarEvent.setIncludeTime(bool includeTime) = const factory DateCellEditorEvent.setIncludeTime(bool includeTime) =
_IncludeTime; _IncludeTime;
const factory DateCellCalendarEvent.setIsRange(bool isRange) = _IsRange; const factory DateCellEditorEvent.setIsRange(bool isRange) = _IsRange;
// date field type options are modified // date field type options are modified
const factory DateCellCalendarEvent.setTimeFormat(TimeFormatPB timeFormat) = const factory DateCellEditorEvent.setTimeFormat(TimeFormatPB timeFormat) =
_TimeFormat; _TimeFormat;
const factory DateCellCalendarEvent.setDateFormat(DateFormatPB dateFormat) = const factory DateCellEditorEvent.setDateFormat(DateFormatPB dateFormat) =
_DateFormat; _DateFormat;
const factory DateCellCalendarEvent.clearDate() = _ClearDate; const factory DateCellEditorEvent.clearDate() = _ClearDate;
} }
@freezed @freezed
class DateCellCalendarState with _$DateCellCalendarState { class DateCellEditorState with _$DateCellEditorState {
const factory DateCellCalendarState({ const factory DateCellEditorState({
// the date field's type option // the date field's type option
required DateTypeOptionPB dateTypeOptionPB, required DateTypeOptionPB dateTypeOptionPB,
@ -413,15 +411,14 @@ class DateCellCalendarState with _$DateCellCalendarState {
required String? parseTimeError, required String? parseTimeError,
required String? parseEndTimeError, required String? parseEndTimeError,
required String timeHintText, required String timeHintText,
}) = _DateCellCalendarState; }) = _DateCellEditorState;
factory DateCellCalendarState.initial( factory DateCellEditorState.initial(DateCellController controller) {
DateTypeOptionPB dateTypeOptionPB, final typeOption = controller.getTypeOption(DateTypeOptionDataParser());
DateCellDataPB? cellData, final cellData = controller.getCellData();
) {
final dateCellData = _dateDataFromCellData(cellData); final dateCellData = _dateDataFromCellData(cellData);
return DateCellCalendarState( return DateCellEditorState(
dateTypeOptionPB: dateTypeOptionPB, dateTypeOptionPB: typeOption,
startDay: dateCellData.isRange ? dateCellData.dateTime : null, startDay: dateCellData.isRange ? dateCellData.dateTime : null,
endDay: dateCellData.isRange ? dateCellData.endDateTime : null, endDay: dateCellData.isRange ? dateCellData.endDateTime : null,
dateTime: dateCellData.dateTime, dateTime: dateCellData.dateTime,
@ -434,7 +431,7 @@ class DateCellCalendarState with _$DateCellCalendarState {
isRange: dateCellData.isRange, isRange: dateCellData.isRange,
parseTimeError: null, parseTimeError: null,
parseEndTimeError: null, parseEndTimeError: null,
timeHintText: _timeHintText(dateTypeOptionPB), timeHintText: _timeHintText(typeOption),
); );
} }
} }
@ -450,13 +447,13 @@ String _timeHintText(DateTypeOptionPB typeOption) {
} }
} }
DateCellData _dateDataFromCellData( _DateCellData _dateDataFromCellData(
DateCellDataPB? cellData, DateCellDataPB? cellData,
) { ) {
// a null DateCellDataPB may be returned, indicating that all the fields are // a null DateCellDataPB may be returned, indicating that all the fields are
// their default values: empty strings and false booleans // their default values: empty strings and false booleans
if (cellData == null) { if (cellData == null) {
return DateCellData( return _DateCellData(
dateTime: null, dateTime: null,
endDateTime: null, endDateTime: null,
timeStr: null, timeStr: null,
@ -492,7 +489,7 @@ DateCellData _dateDataFromCellData(
} }
final String dateStr = cellData.date; final String dateStr = cellData.date;
return DateCellData( return _DateCellData(
dateTime: dateTime, dateTime: dateTime,
endDateTime: endDateTime, endDateTime: endDateTime,
timeStr: timeStr, timeStr: timeStr,
@ -504,7 +501,7 @@ DateCellData _dateDataFromCellData(
); );
} }
class DateCellData { class _DateCellData {
final DateTime? dateTime; final DateTime? dateTime;
final DateTime? endDateTime; final DateTime? endDateTime;
final String? timeStr; final String? timeStr;
@ -514,7 +511,7 @@ class DateCellData {
final String? dateStr; final String? dateStr;
final String? endDateStr; final String? endDateStr;
DateCellData({ _DateCellData({
required this.dateTime, required this.dateTime,
required this.endDateTime, required this.endDateTime,
required this.timeStr, required this.timeStr,

View File

@ -1,16 +1,10 @@
import 'package:flutter/material.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/workspace/presentation/widgets/date_picker/appflowy_date_picker.dart'; import 'package:appflowy/workspace/presentation/widgets/date_picker/appflowy_date_picker.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/date_entities.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:dartz/dartz.dart' show Either; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'date_cal_bloc.dart'; import 'date_cell_editor_bloc.dart';
class DateCellEditor extends StatefulWidget { class DateCellEditor extends StatefulWidget {
const DateCellEditor({ const DateCellEditor({
@ -27,48 +21,6 @@ class DateCellEditor extends StatefulWidget {
} }
class _DateCellEditor extends State<DateCellEditor> { class _DateCellEditor extends State<DateCellEditor> {
@override
Widget build(BuildContext context) {
return FutureBuilder<Either<dynamic, FlowyError>>(
future: widget.cellController.getTypeOption(DateTypeOptionDataParser()),
builder: (_, snapshot) {
if (snapshot.hasData) {
return _buildWidget(snapshot);
}
return const SizedBox.shrink();
},
);
}
Widget _buildWidget(AsyncSnapshot<Either<dynamic, FlowyError>> snapshot) {
return snapshot.data!.fold(
(dateTypeOptionPB) => _CellCalendarWidget(
cellContext: widget.cellController,
dateTypeOptionPB: dateTypeOptionPB,
),
(err) {
Log.error(err);
return const SizedBox.shrink();
},
);
}
}
class _CellCalendarWidget extends StatefulWidget {
final DateCellController cellContext;
final DateTypeOptionPB dateTypeOptionPB;
const _CellCalendarWidget({
required this.cellContext,
required this.dateTypeOptionPB,
});
@override
State<_CellCalendarWidget> createState() => _CellCalendarWidgetState();
}
class _CellCalendarWidgetState extends State<_CellCalendarWidget> {
final PopoverMutex popoverMutex = PopoverMutex(); final PopoverMutex popoverMutex = PopoverMutex();
@override @override
@ -80,22 +32,19 @@ class _CellCalendarWidgetState extends State<_CellCalendarWidget> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return BlocProvider( return BlocProvider(
create: (context) => DateCellCalendarBloc( create: (context) => DateCellEditorBloc(
dateTypeOptionPB: widget.dateTypeOptionPB, cellController: widget.cellController,
cellData: widget.cellContext.getCellData(), )..add(const DateCellEditorEvent.initial()),
cellController: widget.cellContext, child: BlocBuilder<DateCellEditorBloc, DateCellEditorState>(
)..add(const DateCellCalendarEvent.initial()),
child: BlocBuilder<DateCellCalendarBloc, DateCellCalendarState>(
builder: (context, state) { builder: (context, state) {
final bloc = context.read<DateCellEditorBloc>();
return AppFlowyDatePicker( return AppFlowyDatePicker(
includeTime: state.includeTime, includeTime: state.includeTime,
onIncludeTimeChanged: (value) => context onIncludeTimeChanged: (value) =>
.read<DateCellCalendarBloc>() bloc.add(DateCellEditorEvent.setIncludeTime(!value)),
.add(DateCellCalendarEvent.setIncludeTime(!value)),
isRange: state.isRange, isRange: state.isRange,
onIsRangeChanged: (value) => context onIsRangeChanged: (value) =>
.read<DateCellCalendarBloc>() bloc.add(DateCellEditorEvent.setIsRange(!value)),
.add(DateCellCalendarEvent.setIsRange(!value)),
dateFormat: state.dateTypeOptionPB.dateFormat, dateFormat: state.dateTypeOptionPB.dateFormat,
timeFormat: state.dateTypeOptionPB.timeFormat, timeFormat: state.dateTypeOptionPB.timeFormat,
selectedDay: state.dateTime, selectedDay: state.dateTime,
@ -105,28 +54,28 @@ class _CellCalendarWidgetState extends State<_CellCalendarWidget> {
parseEndTimeError: state.parseEndTimeError, parseEndTimeError: state.parseEndTimeError,
parseTimeError: state.parseTimeError, parseTimeError: state.parseTimeError,
popoverMutex: popoverMutex, popoverMutex: popoverMutex,
onStartTimeSubmitted: (timeStr) => context onStartTimeSubmitted: (timeStr) {
.read<DateCellCalendarBloc>() bloc.add(DateCellEditorEvent.setTime(timeStr));
.add(DateCellCalendarEvent.setTime(timeStr)), },
onEndTimeSubmitted: (timeStr) => context onEndTimeSubmitted: (timeStr) {
.read<DateCellCalendarBloc>() bloc.add(DateCellEditorEvent.setEndTime(timeStr));
.add(DateCellCalendarEvent.setEndTime(timeStr)), },
onDaySelected: (selectedDay, _) => context onDaySelected: (selectedDay, _) {
.read<DateCellCalendarBloc>() bloc.add(DateCellEditorEvent.selectDay(selectedDay));
.add(DateCellCalendarEvent.selectDay(selectedDay)), },
onRangeSelected: (start, end, _) => context onRangeSelected: (start, end, _) {
.read<DateCellCalendarBloc>() bloc.add(DateCellEditorEvent.selectDateRange(start, end));
.add(DateCellCalendarEvent.selectDateRange(start, end)), },
allowFormatChanges: true, allowFormatChanges: true,
onDateFormatChanged: (format) => context onDateFormatChanged: (format) {
.read<DateCellCalendarBloc>() bloc.add(DateCellEditorEvent.setDateFormat(format));
.add(DateCellCalendarEvent.setDateFormat(format)), },
onTimeFormatChanged: (format) => context onTimeFormatChanged: (format) {
.read<DateCellCalendarBloc>() bloc.add(DateCellEditorEvent.setTimeFormat(format));
.add(DateCellCalendarEvent.setTimeFormat(format)), },
onClearDate: () => context onClearDate: () {
.read<DateCellCalendarBloc>() bloc.add(const DateCellEditorEvent.clearDate());
.add(const DateCellCalendarEvent.clearDate()), },
); );
}, },
), ),

View File

@ -7,7 +7,7 @@ import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:table_calendar/table_calendar.dart'; import 'package:table_calendar/table_calendar.dart';
import 'date_cal_bloc.dart'; import 'date_cell_editor_bloc.dart';
class MobileDatePicker extends StatefulWidget { class MobileDatePicker extends StatefulWidget {
const MobileDatePicker({ const MobileDatePicker({
@ -49,7 +49,7 @@ class _MobileDatePickerState extends State<MobileDatePicker> {
return Padding( return Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0), padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: BlocBuilder<DateCellCalendarBloc, DateCellCalendarState>( child: BlocBuilder<DateCellEditorBloc, DateCellEditorState>(
builder: (context, state) { builder: (context, state) {
return TableCalendar( return TableCalendar(
firstDay: kFirstDay, firstDay: kFirstDay,
@ -124,13 +124,13 @@ class _MobileDatePickerState extends State<MobileDatePicker> {
selectedDayPredicate: (day) => selectedDayPredicate: (day) =>
state.isRange ? false : isSameDay(state.dateTime, day), state.isRange ? false : isSameDay(state.dateTime, day),
onDaySelected: (selectedDay, focusedDay) { onDaySelected: (selectedDay, focusedDay) {
context.read<DateCellCalendarBloc>().add( context.read<DateCellEditorBloc>().add(
DateCellCalendarEvent.selectDay(selectedDay), DateCellEditorEvent.selectDay(selectedDay),
); );
}, },
onRangeSelected: (start, end, focusedDay) { onRangeSelected: (start, end, focusedDay) {
context.read<DateCellCalendarBloc>().add( context.read<DateCellEditorBloc>().add(
DateCellCalendarEvent.selectDateRange(start, end), DateCellEditorEvent.selectDateRange(start, end),
); );
}, },
onFormatChanged: (calendarFormat) => setState(() { onFormatChanged: (calendarFormat) => setState(() {

View File

@ -15,7 +15,7 @@ import 'package:textfield_tags/textfield_tags.dart';
import '../../../../grid/presentation/layout/sizes.dart'; import '../../../../grid/presentation/layout/sizes.dart';
import '../../../../grid/presentation/widgets/common/type_option_separator.dart'; import '../../../../grid/presentation/widgets/common/type_option_separator.dart';
import '../../../../grid/presentation/widgets/header/type_option/select_option_editor.dart'; import '../../../../grid/presentation/widgets/header/type_option/select/select_option_editor.dart';
import 'extension.dart'; import 'extension.dart';
import 'select_option_editor_bloc.dart'; import 'select_option_editor_bloc.dart';
import 'text_field.dart'; import 'text_field.dart';

View File

@ -353,7 +353,7 @@ class CreateRowFieldButton extends StatefulWidget {
class _CreateRowFieldButtonState extends State<CreateRowFieldButton> { class _CreateRowFieldButtonState extends State<CreateRowFieldButton> {
late PopoverController popoverController; late PopoverController popoverController;
late TypeOptionPB typeOption; FieldPB? createdField;
@override @override
void initState() { void initState() {
@ -383,8 +383,8 @@ class _CreateRowFieldButtonState extends State<CreateRowFieldButton> {
viewId: widget.viewId, viewId: widget.viewId,
); );
result.fold( result.fold(
(l) { (newField) {
typeOption = l; createdField = newField;
popoverController.show(); popoverController.show();
}, },
(r) => Log.error("Failed to create field type option: $r"), (r) => Log.error("Failed to create field type option: $r"),
@ -397,9 +397,12 @@ class _CreateRowFieldButtonState extends State<CreateRowFieldButton> {
), ),
), ),
popupBuilder: (BuildContext popoverContext) { popupBuilder: (BuildContext popoverContext) {
if (createdField == null) {
return const SizedBox.shrink();
}
return FieldEditor( return FieldEditor(
viewId: widget.viewId, viewId: widget.viewId,
field: typeOption.field_2, field: createdField!,
fieldController: widget.fieldController, fieldController: widget.fieldController,
); );
}, },

View File

@ -1,3 +1,4 @@
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/type_option/date/date_time_format.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/common/type_option_separator.dart'; import 'package:appflowy/plugins/database_view/grid/presentation/widgets/common/type_option_separator.dart';
@ -6,7 +7,6 @@ import 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/date
import 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/date_type_option_button.dart'; import 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/date_type_option_button.dart';
import 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/end_text_field.dart'; import 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/end_text_field.dart';
import 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/end_time_button.dart'; import 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/end_time_button.dart';
import 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/include_time_button.dart';
import 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/start_text_field.dart'; import 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/start_text_field.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/date_entities.pbenum.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/date_entities.pbenum.dart';
import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:appflowy_popover/appflowy_popover.dart';

View File

@ -1,10 +1,10 @@
import 'package:appflowy/workspace/presentation/widgets/date_picker/utils/layout.dart'; import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/type_option/date/date_time_format.dart';
import 'package:flutter/material.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/type_option/date.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/date_entities.pbenum.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/date_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 '../utils/layout.dart';
class DateTimeSetting extends StatefulWidget { class DateTimeSetting extends StatefulWidget {
const DateTimeSetting({ const DateTimeSetting({
@ -52,9 +52,9 @@ class _DateTimeSettingState extends State<DateTimeSetting> {
selectedFormat: widget.timeFormat, selectedFormat: widget.timeFormat,
onSelected: _onTimeFormatChanged, onSelected: _onTimeFormatChanged,
), ),
child: Padding( child: const Padding(
padding: const EdgeInsets.symmetric(horizontal: 6.0), padding: EdgeInsets.symmetric(horizontal: 6.0),
child: TimeFormatButton(timeFormat: widget.timeFormat), child: TimeFormatButton(),
), ),
), ),
]; ];

View File

@ -1,48 +0,0 @@
import 'package:appflowy/workspace/presentation/widgets/date_picker/utils/layout.dart';
import 'package:flutter/material.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/workspace/presentation/widgets/toggle/toggle.dart';
import 'package:appflowy/workspace/presentation/widgets/toggle/toggle_style.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flowy_infra_ui/widget/spacing.dart';
class IncludeTimeButton extends StatelessWidget {
const IncludeTimeButton({
super.key,
required this.value,
required this.onChanged,
});
final bool value;
final Function(bool value) onChanged;
@override
Widget build(BuildContext context) {
return SizedBox(
height: DatePickerSize.itemHeight,
child: Padding(
padding: DatePickerSize.itemOptionInsets,
child: Row(
children: [
FlowySvg(
FlowySvgs.clock_alarm_s,
color: Theme.of(context).iconTheme.color,
),
const HSpace(6),
FlowyText.medium(LocaleKeys.datePicker_includeTime.tr()),
const Spacer(),
Toggle(
value: value,
onChanged: onChanged,
style: ToggleStyle.big,
padding: EdgeInsets.zero,
),
],
),
),
);
}
}

View File

@ -1,6 +1,5 @@
import 'package:appflowy/plugins/database_view/application/database_controller.dart'; import 'package:appflowy/plugins/database_view/application/database_controller.dart';
import 'package:appflowy/plugins/database_view/application/field/field_editor_bloc.dart'; import 'package:appflowy/plugins/database_view/application/field/field_editor_bloc.dart';
import 'package:appflowy/plugins/database_view/application/field/type_option/type_option_context.dart';
import 'package:appflowy/plugins/database_view/board/application/board_bloc.dart'; import 'package:appflowy/plugins/database_view/board/application/board_bloc.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
@ -34,14 +33,9 @@ void main() {
await boardResponseFuture(); await boardResponseFuture();
final fieldInfo = context.singleSelectFieldContext(); final fieldInfo = context.singleSelectFieldContext();
final loader = FieldTypeOptionLoader(
viewId: context.gridView.id,
field: fieldInfo.field,
);
final editorBloc = FieldEditorBloc( final editorBloc = FieldEditorBloc(
viewId: context.gridView.id, viewId: context.gridView.id,
loader: loader,
field: fieldInfo.field, field: fieldInfo.field,
fieldController: context.fieldController, fieldController: context.fieldController,
)..add(const FieldEditorEvent.initial()); )..add(const FieldEditorEvent.initial());

View File

@ -4,7 +4,6 @@ import 'package:appflowy/plugins/database_view/application/database_controller.d
import 'package:appflowy/plugins/database_view/application/field/field_controller.dart'; import 'package:appflowy/plugins/database_view/application/field/field_controller.dart';
import 'package:appflowy/plugins/database_view/application/field/field_editor_bloc.dart'; import 'package:appflowy/plugins/database_view/application/field/field_editor_bloc.dart';
import 'package:appflowy/plugins/database_view/application/field/field_info.dart'; import 'package:appflowy/plugins/database_view/application/field/field_info.dart';
import 'package:appflowy/plugins/database_view/application/field/type_option/type_option_context.dart';
import 'package:appflowy/plugins/database_view/application/row/row_cache.dart'; import 'package:appflowy/plugins/database_view/application/row/row_cache.dart';
import 'package:appflowy/plugins/database_view/application/row/row_controller.dart'; import 'package:appflowy/plugins/database_view/application/row/row_controller.dart';
import 'package:appflowy/plugins/database_view/board/board.dart'; import 'package:appflowy/plugins/database_view/board/board.dart';
@ -82,15 +81,9 @@ class BoardTestContext {
FieldEditorBloc makeFieldEditor({ FieldEditorBloc makeFieldEditor({
required FieldInfo fieldInfo, required FieldInfo fieldInfo,
}) { }) {
final loader = FieldTypeOptionLoader(
viewId: gridView.id,
field: fieldInfo.field,
);
final editorBloc = FieldEditorBloc( final editorBloc = FieldEditorBloc(
viewId: databaseController.viewId, viewId: databaseController.viewId,
fieldController: fieldController, fieldController: fieldController,
loader: loader,
field: fieldInfo.field, field: fieldInfo.field,
); );
return editorBloc; return editorBloc;

View File

@ -1,21 +1,15 @@
import 'package:appflowy/plugins/database_view/application/field/field_editor_bloc.dart'; import 'package:appflowy/plugins/database_view/application/field/field_editor_bloc.dart';
import 'package:appflowy/plugins/database_view/application/field/type_option/type_option_context.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import '../util.dart'; import '../util.dart';
Future<FieldEditorBloc> createEditorBloc(AppFlowyGridTest gridTest) async { Future<FieldEditorBloc> createEditorBloc(AppFlowyGridTest gridTest) async {
final context = await gridTest.createTestGrid(); final context = await gridTest.createTestGrid();
final fieldInfo = context.singleSelectFieldContext(); final fieldInfo = context.singleSelectFieldContext();
final loader = FieldTypeOptionLoader(
viewId: context.gridView.id,
field: fieldInfo.field,
);
return FieldEditorBloc( return FieldEditorBloc(
viewId: context.gridView.id, viewId: context.gridView.id,
fieldController: context.fieldController, fieldController: context.fieldController,
loader: loader,
field: fieldInfo.field, field: fieldInfo.field,
)..add(const FieldEditorEvent.initial()); )..add(const FieldEditorEvent.initial());
} }

View File

@ -4,7 +4,6 @@ import 'package:appflowy/plugins/database_view/application/field/field_controlle
import 'package:appflowy/plugins/database_view/application/field/field_editor_bloc.dart'; import 'package:appflowy/plugins/database_view/application/field/field_editor_bloc.dart';
import 'package:appflowy/plugins/database_view/application/field/field_info.dart'; import 'package:appflowy/plugins/database_view/application/field/field_info.dart';
import 'package:appflowy/plugins/database_view/application/field/field_service.dart'; import 'package:appflowy/plugins/database_view/application/field/field_service.dart';
import 'package:appflowy/plugins/database_view/application/field/type_option/type_option_context.dart';
import 'package:appflowy/plugins/database_view/application/row/row_cache.dart'; import 'package:appflowy/plugins/database_view/application/row/row_cache.dart';
import 'package:appflowy/plugins/database_view/application/row/row_controller.dart'; import 'package:appflowy/plugins/database_view/application/row/row_controller.dart';
import 'package:appflowy/plugins/database_view/application/database_controller.dart'; import 'package:appflowy/plugins/database_view/application/database_controller.dart';
@ -134,16 +133,11 @@ Future<FieldEditorBloc> createFieldEditor({
); );
await gridResponseFuture(); await gridResponseFuture();
return result.fold( return result.fold(
(data) { (field) {
final loader = FieldTypeOptionLoader(
viewId: databaseController.viewId,
field: data.field_2,
);
return FieldEditorBloc( return FieldEditorBloc(
viewId: databaseController.viewId, viewId: databaseController.viewId,
fieldController: databaseController.fieldController, fieldController: databaseController.fieldController,
loader: loader, field: field,
field: data.field_2,
); );
}, },
(err) => throw Exception(err), (err) => throw Exception(err),

View File

@ -14,7 +14,7 @@ export const useDateTimeFormat = (cellIdentifier: CellIdentifier, fieldControlle
await typeOptionController.initialize(); await typeOptionController.initialize();
const dateTypeOptionContext = makeDateTypeOptionContext(typeOptionController); const dateTypeOptionContext = makeDateTypeOptionContext(typeOptionController);
const typeOption = await dateTypeOptionContext.getTypeOption().then((a) => a.unwrap()); const typeOption = dateTypeOptionContext.getTypeOption();
change(typeOption); change(typeOption);
await dateTypeOptionContext.setTypeOption(typeOption); await dateTypeOptionContext.setTypeOption(typeOption);

View File

@ -14,7 +14,7 @@ export const useNumberFormat = (cellIdentifier: CellIdentifier, fieldController:
await typeOptionController.initialize(); await typeOptionController.initialize();
const numberTypeOptionContext = makeNumberTypeOptionContext(typeOptionController); const numberTypeOptionContext = makeNumberTypeOptionContext(typeOptionController);
const typeOption = await numberTypeOptionContext.getTypeOption().then((a) => a.unwrap()); const typeOption = numberTypeOptionContext.getTypeOption();
typeOption.format = format; typeOption.format = format;
await numberTypeOptionContext.setTypeOption(typeOption); await numberTypeOptionContext.setTypeOption(typeOption);

View File

@ -26,7 +26,7 @@ export default async function (viewId: string, fieldInfo: FieldInfo, dispatch?:
let typeOption: SingleSelectTypeOptionPB | MultiSelectTypeOptionPB | undefined; let typeOption: SingleSelectTypeOptionPB | MultiSelectTypeOptionPB | undefined;
if (field.field_type === FieldType.SingleSelect) { if (field.field_type === FieldType.SingleSelect) {
typeOption = (await makeSingleSelectTypeOptionContext(typeOptionController).getTypeOption()).unwrap(); typeOption = makeSingleSelectTypeOptionContext(typeOptionController).getTypeOption();
if (!groupingFieldSelected) { if (!groupingFieldSelected) {
if (dispatch) { if (dispatch) {
dispatch(boardActions.setGroupingFieldId({ fieldId: field.id })); dispatch(boardActions.setGroupingFieldId({ fieldId: field.id }));
@ -37,7 +37,7 @@ export default async function (viewId: string, fieldInfo: FieldInfo, dispatch?:
} }
if (field.field_type === FieldType.MultiSelect) { if (field.field_type === FieldType.MultiSelect) {
typeOption = (await makeMultiSelectTypeOptionContext(typeOptionController).getTypeOption()).unwrap(); typeOption = makeMultiSelectTypeOptionContext(typeOptionController).getTypeOption();
} }
if (typeOption) { if (typeOption) {
@ -63,7 +63,7 @@ export default async function (viewId: string, fieldInfo: FieldInfo, dispatch?:
} }
case FieldType.Number: { case FieldType.Number: {
const typeOption = (await makeNumberTypeOptionContext(typeOptionController).getTypeOption()).unwrap(); const typeOption = makeNumberTypeOptionContext(typeOptionController).getTypeOption();
return { return {
fieldId: field.id, fieldId: field.id,
@ -78,7 +78,7 @@ export default async function (viewId: string, fieldInfo: FieldInfo, dispatch?:
} }
case FieldType.DateTime: { case FieldType.DateTime: {
const typeOption = (await makeDateTypeOptionContext(typeOptionController).getTypeOption()).unwrap(); const typeOption = makeDateTypeOptionContext(typeOptionController).getTypeOption();
return { return {
fieldId: field.id, fieldId: field.id,

View File

@ -25,7 +25,7 @@ import {
DatabaseEventGetAllFieldSettings, DatabaseEventGetAllFieldSettings,
} from '@/services/backend/events/flowy-database2'; } from '@/services/backend/events/flowy-database2';
import { Field, pbToField } from './field_types'; import { Field, pbToField } from './field_types';
import { getTypeOption } from './type_option'; import { bytesToTypeOption } from './type_option';
import { Database } from '$app/components/database/application'; import { Database } from '$app/components/database/application';
export async function getFields( export async function getFields(
@ -64,7 +64,7 @@ export async function getFields(
const field = pbToField(item); const field = pbToField(item);
const typeOption = await getTypeOption(viewId, item.id, item.field_type); const typeOption = bytesToTypeOption(item.type_option_data, item.field_type);
if (typeOption) { if (typeOption) {
typeOptions[item.id] = typeOption; typeOptions[item.id] = typeOption;
@ -110,7 +110,7 @@ export async function createField({
return Promise.reject('Failed to create field'); return Promise.reject('Failed to create field');
} }
return pbToField(result.val.field); return pbToField(result.val);
} }
export async function duplicateField(viewId: string, fieldId: string): Promise<void> { export async function duplicateField(viewId: string, fieldId: string): Promise<void> {

View File

@ -1,27 +1,8 @@
import { FieldType, TypeOptionPathPB, TypeOptionChangesetPB } from '@/services/backend'; import { FieldType, TypeOptionChangesetPB } from '@/services/backend';
import { import {
DatabaseEventGetTypeOption,
DatabaseEventUpdateFieldTypeOption, DatabaseEventUpdateFieldTypeOption,
} from '@/services/backend/events/flowy-database2'; } from '@/services/backend/events/flowy-database2';
import { bytesToTypeOption, UndeterminedTypeOptionData, typeOptionDataToPB } from './type_option_types'; import { UndeterminedTypeOptionData, typeOptionDataToPB } from './type_option_types';
export async function getTypeOption(viewId: string, fieldId: string, fieldType: FieldType) {
const payload = TypeOptionPathPB.fromObject({
view_id: viewId,
field_id: fieldId,
field_type: fieldType,
});
const result = await DatabaseEventGetTypeOption(payload);
if (!result.ok) {
return Promise.reject(result.val);
}
const value = result.val;
return bytesToTypeOption(value.type_option_data, fieldType);
}
export async function updateTypeOption( export async function updateTypeOption(
viewId: string, viewId: string,

View File

@ -11,7 +11,6 @@ import {
URLCellController, URLCellController,
} from '$app/stores/effects/database/cell/controller_builder'; } from '$app/stores/effects/database/cell/controller_builder';
import { None, Option, Some } from 'ts-results'; import { None, Option, Some } from 'ts-results';
import { TypeOptionBackendService } from '$app/stores/effects/database/field/type_option/type_option_bd_svc';
import { DatabaseBackendService } from '$app/stores/effects/database/database_bd_svc'; import { DatabaseBackendService } from '$app/stores/effects/database/database_bd_svc';
import { FieldInfo } from '$app/stores/effects/database/field/field_controller'; import { FieldInfo } from '$app/stores/effects/database/field/field_controller';
import { TypeOptionController } from '$app/stores/effects/database/field/type_option/type_option_controller'; import { TypeOptionController } from '$app/stores/effects/database/field/type_option/type_option_controller';
@ -175,12 +174,11 @@ export function findFirstFieldInfoWithFieldType(rowInfo: RowInfo, fieldType: Fie
} }
} }
export async function assertFieldName(viewId: string, fieldId: string, fieldType: FieldType, expected: string) { export async function assertFieldName(controller: TypeOptionController, expected: string) {
const svc = new TypeOptionBackendService(viewId); const fieldInfo = controller.getFieldInfo();
const typeOptionPB = await svc.getTypeOption(fieldId, fieldType).then((result) => result.unwrap());
if (typeOptionPB.field.name !== expected) { if (fieldInfo.field.name !== expected) {
throw Error('Expect field name:' + expected + 'but receive:' + typeOptionPB.field.name); throw Error('Expect field name:' + expected + 'but receive:' + fieldInfo.field.name);
} }
} }
@ -218,9 +216,8 @@ export async function createSingleSelectOptions(viewId: string, fieldInfo: Field
assert(fieldInfo.field.field_type === FieldType.SingleSelect, 'Only work on single select'); assert(fieldInfo.field.field_type === FieldType.SingleSelect, 'Only work on single select');
const typeOptionController = new TypeOptionController(viewId, Some(fieldInfo)); const typeOptionController = new TypeOptionController(viewId, Some(fieldInfo));
const singleSelectTypeOptionContext = makeSingleSelectTypeOptionContext(typeOptionController); const singleSelectTypeOptionContext = makeSingleSelectTypeOptionContext(typeOptionController);
const singleSelectTypeOptionPB: SingleSelectTypeOptionPB = await singleSelectTypeOptionContext const singleSelectTypeOptionPB: SingleSelectTypeOptionPB = singleSelectTypeOptionContext
.getTypeOption() .getTypeOption();
.then((result) => result.unwrap());
const backendSvc = new SelectOptionBackendService(viewId, fieldInfo.field.id); const backendSvc = new SelectOptionBackendService(viewId, fieldInfo.field.id);

View File

@ -195,7 +195,7 @@ async function testEditDateFormatPB() {
// update date type option // update date type option
const dateTypeOptionContext = makeDateTypeOptionContext(typeOptionController); const dateTypeOptionContext = makeDateTypeOptionContext(typeOptionController);
const typeOption = await dateTypeOptionContext.getTypeOption().then((a) => a.unwrap()); const typeOption = dateTypeOptionContext.getTypeOption();
assert(typeOption.date_format === DateFormatPB.Friendly, 'Date format not match'); assert(typeOption.date_format === DateFormatPB.Friendly, 'Date format not match');
assert(typeOption.time_format === TimeFormatPB.TwentyFourHour, 'Time format not match'); assert(typeOption.time_format === TimeFormatPB.TwentyFourHour, 'Time format not match');
@ -203,7 +203,7 @@ async function testEditDateFormatPB() {
typeOption.time_format = TimeFormatPB.TwelveHour; typeOption.time_format = TimeFormatPB.TwelveHour;
await dateTypeOptionContext.setTypeOption(typeOption); await dateTypeOptionContext.setTypeOption(typeOption);
const typeOption2 = await dateTypeOptionContext.getTypeOption().then((a) => a.unwrap()); const typeOption2 = dateTypeOptionContext.getTypeOption();
assert(typeOption2.date_format === DateFormatPB.Local, 'Date format not match'); assert(typeOption2.date_format === DateFormatPB.Local, 'Date format not match');
assert(typeOption2.time_format === TimeFormatPB.TwelveHour, 'Time format not match'); assert(typeOption2.time_format === TimeFormatPB.TwelveHour, 'Time format not match');
@ -224,13 +224,13 @@ async function testEditNumberFormatPB() {
// update date type option // update date type option
const dateTypeOptionContext = makeNumberTypeOptionContext(typeOptionController); const dateTypeOptionContext = makeNumberTypeOptionContext(typeOptionController);
const typeOption = await dateTypeOptionContext.getTypeOption().then((a) => a.unwrap()); const typeOption = dateTypeOptionContext.getTypeOption();
typeOption.format = NumberFormatPB.EUR; typeOption.format = NumberFormatPB.EUR;
typeOption.name = 'Money'; typeOption.name = 'Money';
await dateTypeOptionContext.setTypeOption(typeOption); await dateTypeOptionContext.setTypeOption(typeOption);
const typeOption2 = await dateTypeOptionContext.getTypeOption().then((a) => a.unwrap()); const typeOption2 = dateTypeOptionContext.getTypeOption();
Log.info(typeOption2); Log.info(typeOption2);
await new Promise((resolve) => setTimeout(resolve, 200)); await new Promise((resolve) => setTimeout(resolve, 200));
@ -377,7 +377,7 @@ async function testGetSingleSelectFieldData() {
]); ]);
// Read options // Read options
const options = await singleSelectTypeOptionContext.getTypeOption().then((result) => result.unwrap()); const options = singleSelectTypeOptionContext.getTypeOption();
console.log(options); console.log(options);
@ -401,9 +401,8 @@ async function testSwitchFromSingleSelectToNumber() {
// Check the number type option // Check the number type option
const numberTypeOptionContext = makeNumberTypeOptionContext(typeOptionController); const numberTypeOptionContext = makeNumberTypeOptionContext(typeOptionController);
const numberTypeOption: NumberTypeOptionPB = await numberTypeOptionContext const numberTypeOption: NumberTypeOptionPB = numberTypeOptionContext
.getTypeOption() .getTypeOption();
.then((result) => result.unwrap());
const format: NumberFormatPB = numberTypeOption.format; const format: NumberFormatPB = numberTypeOption.format;
if (format !== NumberFormatPB.Num) { if (format !== NumberFormatPB.Num) {
@ -452,7 +451,7 @@ async function testSwitchFromMultiSelectToRichText() {
await selectOptionCellController.dispose(); await selectOptionCellController.dispose();
// Switch to RichText field type // Switch to RichText field type
await typeOptionController.switchToField(FieldType.RichText).then((result) => result.unwrap()); await typeOptionController.switchToField(FieldType.RichText);
if (typeOptionController.fieldType !== FieldType.RichText) { if (typeOptionController.fieldType !== FieldType.RichText) {
throw Error('The field type should be text'); throw Error('The field type should be text');
} }
@ -486,7 +485,7 @@ async function testEditField() {
await controller.setFieldName(newName); await controller.setFieldName(newName);
await new Promise((resolve) => setTimeout(resolve, 200)); await new Promise((resolve) => setTimeout(resolve, 200));
await assertFieldName(view.id, firstFieldInfo.field.id, firstFieldInfo.field.field_type, newName); await assertFieldName(controller, newName);
await databaseController.dispose(); await databaseController.dispose();
} }

View File

@ -2,11 +2,11 @@ import { CellIdentifier } from './cell_bd_svc';
import { CellCache, CellCacheKey } from './cell_cache'; import { CellCache, CellCacheKey } from './cell_cache';
import { CellDataLoader } from './data_parser'; import { CellDataLoader } from './data_parser';
import { CellDataPersistence } from './data_persistence'; import { CellDataPersistence } from './data_persistence';
import { FieldBackendService, TypeOptionParser } from '../field/field_bd_svc'; import { FieldBackendService } from '../field/field_bd_svc';
import { ChangeNotifier } from '$app/utils/change_notifier'; import { ChangeNotifier } from '$app/utils/change_notifier';
import { CellObserver } from './cell_observer'; import { CellObserver } from './cell_observer';
import { Log } from '$app/utils/log'; import { Log } from '$app/utils/log';
import { Err, None, Ok, Option, Some } from 'ts-results'; import { None, Option, Some } from 'ts-results';
import { DatabaseFieldObserver } from '../field/field_observer'; import { DatabaseFieldObserver } from '../field/field_observer';
type Callbacks<T> = { onCellChanged: (value: Option<T>) => void; onFieldChanged?: () => void }; type Callbacks<T> = { onCellChanged: (value: Option<T>) => void; onFieldChanged?: () => void };
@ -70,16 +70,6 @@ export class CellController<T, D> {
}); });
}; };
getTypeOption = async <P extends TypeOptionParser<PD>, PD>(parser: P) => {
const result = await this.fieldBackendService.getTypeOptionData(this.cellIdentifier.fieldType);
if (result.ok) {
return Ok(parser.fromBuffer(result.val.type_option_data));
} else {
return Err(result.val);
}
};
saveCellData = async (data: D) => { saveCellData = async (data: D) => {
const result = await this.cellDataPersistence.save(data); const result = await this.cellDataPersistence.save(data);

View File

@ -2,14 +2,11 @@ import {
DeleteFieldPayloadPB, DeleteFieldPayloadPB,
DuplicateFieldPayloadPB, DuplicateFieldPayloadPB,
FieldChangesetPB, FieldChangesetPB,
FieldType,
TypeOptionChangesetPB, TypeOptionChangesetPB,
TypeOptionPathPB,
} from '@/services/backend'; } from '@/services/backend';
import { import {
DatabaseEventDeleteField, DatabaseEventDeleteField,
DatabaseEventDuplicateField, DatabaseEventDuplicateField,
DatabaseEventGetTypeOption,
DatabaseEventUpdateField, DatabaseEventUpdateField,
DatabaseEventUpdateFieldTypeOption, DatabaseEventUpdateFieldTypeOption,
} from '@/services/backend/events/flowy-database2'; } from '@/services/backend/events/flowy-database2';
@ -64,14 +61,4 @@ export class FieldBackendService {
return DatabaseEventDuplicateField(payload); return DatabaseEventDuplicateField(payload);
}; };
getTypeOptionData = (fieldType: FieldType) => {
const payload = TypeOptionPathPB.fromObject({
view_id: this.viewId,
field_id: this.fieldId,
field_type: fieldType,
});
return DatabaseEventGetTypeOption(payload);
};
} }

View File

@ -1,7 +1,6 @@
import { CreateFieldPayloadPB, FieldType, TypeOptionPathPB, UpdateFieldTypePayloadPB } from '@/services/backend'; import { CreateFieldPayloadPB, FieldType, UpdateFieldTypePayloadPB } from '@/services/backend';
import { import {
DatabaseEventCreateField, DatabaseEventCreateField,
DatabaseEventGetTypeOption,
DatabaseEventUpdateFieldType, DatabaseEventUpdateFieldType,
} from '@/services/backend/events/flowy-database2'; } from '@/services/backend/events/flowy-database2';
@ -14,16 +13,6 @@ export class TypeOptionBackendService {
return DatabaseEventCreateField(payload); return DatabaseEventCreateField(payload);
}; };
getTypeOption = (fieldId: string, fieldType: FieldType) => {
const payload = TypeOptionPathPB.fromObject({
view_id: this.viewId,
field_id: fieldId,
field_type: fieldType,
});
return DatabaseEventGetTypeOption(payload);
};
updateTypeOptionType = (fieldId: string, fieldType: FieldType) => { updateTypeOptionType = (fieldId: string, fieldType: FieldType) => {
const payload = UpdateFieldTypePayloadPB.fromObject({ const payload = UpdateFieldTypePayloadPB.fromObject({
view_id: this.viewId, view_id: this.viewId,

View File

@ -1,10 +1,9 @@
import { None, Ok, Option, Result, Some } from 'ts-results'; import { None, Option, Some } from 'ts-results';
import { TypeOptionController } from './type_option_controller'; import { TypeOptionController } from './type_option_controller';
import { import {
CheckboxTypeOptionPB, CheckboxTypeOptionPB,
ChecklistTypeOptionPB, ChecklistTypeOptionPB,
DateTypeOptionPB, DateTypeOptionPB,
FlowyError,
MultiSelectTypeOptionPB, MultiSelectTypeOptionPB,
NumberTypeOptionPB, NumberTypeOptionPB,
SingleSelectTypeOptionPB, SingleSelectTypeOptionPB,
@ -190,17 +189,12 @@ export class TypeOptionContext<T> {
return this.controller.viewId; return this.controller.viewId;
} }
getTypeOption = async (): Promise<Result<T, FlowyError>> => { getTypeOption = (): T => {
const result = await this.controller.getTypeOption(); const type_option_data = this.controller.getTypeOption();
const typeOption = this.parser.deserialize(type_option_data);
if (result.ok) { this.typeOption = Some(typeOption);
const typeOption = this.parser.deserialize(result.val.type_option_data); return typeOption;
this.typeOption = Some(typeOption);
return Ok(typeOption);
} else {
return result;
}
}; };
// Save the typeOption to disk // Save the typeOption to disk

View File

@ -1,4 +1,4 @@
import { FieldPB, FieldType, TypeOptionPB } from '@/services/backend'; import { FieldPB, FieldType } from '@/services/backend';
import { ChangeNotifier } from '$app/utils/change_notifier'; import { ChangeNotifier } from '$app/utils/change_notifier';
import { FieldBackendService } from '../field_bd_svc'; import { FieldBackendService } from '../field_bd_svc';
import { Log } from '$app/utils/log'; import { Log } from '$app/utils/log';
@ -8,7 +8,7 @@ import { TypeOptionBackendService } from './type_option_bd_svc';
export class TypeOptionController { export class TypeOptionController {
private fieldNotifier = new ChangeNotifier<FieldPB>(); private fieldNotifier = new ChangeNotifier<FieldPB>();
private typeOptionData: Option<TypeOptionPB>; private field: Option<FieldPB>;
private fieldBackendSvc?: FieldBackendService; private fieldBackendSvc?: FieldBackendService;
private typeOptionBackendSvc: TypeOptionBackendService; private typeOptionBackendSvc: TypeOptionBackendService;
@ -18,7 +18,13 @@ export class TypeOptionController {
private readonly initialFieldInfo: Option<FieldInfo> = None, private readonly initialFieldInfo: Option<FieldInfo> = None,
private readonly defaultFieldType: FieldType = FieldType.RichText private readonly defaultFieldType: FieldType = FieldType.RichText
) { ) {
this.typeOptionData = None; if (initialFieldInfo.none) {
this.field = None;
} else {
this.field = Some(initialFieldInfo.val.field);
this.fieldBackendSvc = new FieldBackendService(this.viewId, initialFieldInfo.val.field.id);
}
this.typeOptionBackendSvc = new TypeOptionBackendService(viewId); this.typeOptionBackendSvc = new TypeOptionBackendService(viewId);
} }
@ -27,8 +33,6 @@ export class TypeOptionController {
initialize = async () => { initialize = async () => {
if (this.initialFieldInfo.none) { if (this.initialFieldInfo.none) {
await this.createTypeOption(this.defaultFieldType); await this.createTypeOption(this.defaultFieldType);
} else {
await this.getTypeOption();
} }
}; };
@ -41,7 +45,7 @@ export class TypeOptionController {
} }
getFieldInfo = (): FieldInfo => { getFieldInfo = (): FieldInfo => {
if (this.typeOptionData.none) { if (this.field.none) {
if (this.initialFieldInfo.some) { if (this.initialFieldInfo.some) {
return this.initialFieldInfo.val; return this.initialFieldInfo.val;
} else { } else {
@ -49,35 +53,31 @@ export class TypeOptionController {
} }
} }
return new FieldInfo(this.typeOptionData.val.field); return new FieldInfo(this.field.val);
}; };
switchToField = async (fieldType: FieldType) => { switchToField = async (fieldType: FieldType) => {
const result = await this.typeOptionBackendSvc.updateTypeOptionType(this.fieldId, fieldType); if (this.field.some) {
this.field.val.field_type = fieldType;
if (result.ok) { await this.typeOptionBackendSvc.updateTypeOptionType(this.fieldId, fieldType).then((result) => {
const getResult = await this.typeOptionBackendSvc.getTypeOption(this.fieldId, fieldType); if (result.err) {
Log.error(result.val);
if (getResult.ok) { }
this.updateTypeOptionData(getResult.val); });
} this.fieldNotifier.notify(this.field.val);
return getResult;
}
return result;
};
setFieldName = async (name: string) => {
if (this.typeOptionData.some) {
this.typeOptionData.val.field.name = name;
void this.fieldBackendSvc?.updateField({ name: name });
this.fieldNotifier.notify(this.typeOptionData.val.field);
} else { } else {
throw Error('Unexpected empty type option data. Should call initialize first'); throw Error('Unexpected empty type option data. Should call initialize first');
} }
}; };
setFieldName = async (name: string) => {
if (this.field.some) {
this.field.val.name = name;
void this.fieldBackendSvc?.updateField({ name: name });
this.fieldNotifier.notify(this.field.val);
}
};
hideField = async () => { hideField = async () => {
if (this.fieldBackendSvc) { if (this.fieldBackendSvc) {
void this.fieldBackendSvc.updateField({ visibility: false }); void this.fieldBackendSvc.updateField({ visibility: false });
@ -103,8 +103,8 @@ export class TypeOptionController {
}; };
saveTypeOption = async (data: Uint8Array) => { saveTypeOption = async (data: Uint8Array) => {
if (this.typeOptionData.some) { if (this.field.some) {
this.typeOptionData.val.type_option_data = data; this.field.val.type_option_data = data;
await this.fieldBackendSvc?.updateTypeOption(data).then((result) => { await this.fieldBackendSvc?.updateTypeOption(data).then((result) => {
if (result.err) { if (result.err) {
Log.error(result.val); Log.error(result.val);
@ -132,29 +132,27 @@ export class TypeOptionController {
}; };
// Returns the type option for specific field with specific fieldType // Returns the type option for specific field with specific fieldType
getTypeOption = async () => { getTypeOption = () => {
return this.typeOptionBackendSvc.getTypeOption(this.fieldId, this.fieldType).then((result) => { if (this.field.some) {
if (result.ok) { return this.field.val.type_option_data;
this.updateTypeOptionData(result.val); } else {
} throw Error('Unexpected empty type option data. Should call initialize first');
}
return result;
});
}; };
private createTypeOption = (fieldType: FieldType) => { private createTypeOption = async (fieldType: FieldType) => {
return this.typeOptionBackendSvc.createTypeOption(fieldType).then((result) => { const result = await this.typeOptionBackendSvc.createTypeOption(fieldType);
if (result.ok) {
this.updateTypeOptionData(result.val);
}
return result; if (result.ok) {
}); this.updateField(result.val);
}
return result;
}; };
private updateTypeOptionData = (typeOptionData: TypeOptionPB) => { private updateField = (field: FieldPB) => {
this.typeOptionData = Some(typeOptionData); this.field = Some(field);
this.fieldBackendSvc = new FieldBackendService(this.viewId, typeOptionData.field.id); this.fieldBackendSvc = new FieldBackendService(this.viewId, field.id);
this.fieldNotifier.notify(typeOptionData.field); this.fieldNotifier.notify(field);
}; };
} }

View File

@ -120,8 +120,7 @@ impl EventIntegrationTest {
}) })
.async_send() .async_send()
.await .await
.parse::<TypeOptionPB>() .parse::<FieldPB>()
.field
} }
pub async fn update_field(&self, changeset: FieldChangesetPB) { pub async fn update_field(&self, changeset: FieldChangesetPB) {

View File

@ -14,6 +14,7 @@ use flowy_error::ErrorCode;
use crate::entities::parser::NotEmptyStr; use crate::entities::parser::NotEmptyStr;
use crate::entities::position_entities::OrderObjectPositionPB; use crate::entities::position_entities::OrderObjectPositionPB;
use crate::impl_into_field_type; use crate::impl_into_field_type;
use crate::services::field::{default_type_option_data_from_type, type_option_to_pb};
/// [FieldPB] defines a Field's attributes. Such as the name, field_type, and width. etc. /// [FieldPB] defines a Field's attributes. Such as the name, field_type, and width. etc.
#[derive(Debug, Clone, Default, ProtoBuf)] #[derive(Debug, Clone, Default, ProtoBuf)]
@ -35,17 +36,25 @@ pub struct FieldPB {
#[pb(index = 6)] #[pb(index = 6)]
pub is_primary: bool, pub is_primary: bool,
#[pb(index = 7)]
pub type_option_data: Vec<u8>,
} }
impl std::convert::From<Field> for FieldPB { impl FieldPB {
fn from(field: Field) -> Self { pub fn new(field: Field) -> Self {
let field_type = field.field_type.into();
let type_option = field
.get_any_type_option(field_type)
.unwrap_or_else(|| default_type_option_data_from_type(&field_type));
Self { Self {
id: field.id, id: field.id,
name: field.name, name: field.name,
field_type: FieldType::from(field.field_type), field_type,
visibility: field.visibility, visibility: field.visibility,
width: field.width as i32, width: field.width as i32,
is_primary: field.is_primary, is_primary: field.is_primary,
type_option_data: type_option_to_pb(type_option, &field_type).to_vec(),
} }
} }
} }
@ -139,15 +148,6 @@ pub struct IndexFieldPB {
pub index: i32, pub index: i32,
} }
impl IndexFieldPB {
pub fn from_field(field: Field, index: usize) -> Self {
Self {
field: FieldPB::from(field),
index: index as i32,
}
}
}
#[derive(Debug, Default, ProtoBuf)] #[derive(Debug, Default, ProtoBuf)]
pub struct CreateFieldPayloadPB { pub struct CreateFieldPayloadPB {
#[pb(index = 1)] #[pb(index = 1)]
@ -236,50 +236,6 @@ impl TryInto<EditFieldParams> for UpdateFieldTypePayloadPB {
} }
} }
#[derive(Debug, Default, ProtoBuf)]
pub struct TypeOptionPathPB {
#[pb(index = 1)]
pub view_id: String,
#[pb(index = 2)]
pub field_id: String,
#[pb(index = 3)]
pub field_type: FieldType,
}
pub struct TypeOptionPathParams {
pub view_id: String,
pub field_id: String,
pub field_type: FieldType,
}
impl TryInto<TypeOptionPathParams> for TypeOptionPathPB {
type Error = ErrorCode;
fn try_into(self) -> Result<TypeOptionPathParams, Self::Error> {
let database_id = NotEmptyStr::parse(self.view_id).map_err(|_| ErrorCode::DatabaseIdIsEmpty)?;
let field_id = NotEmptyStr::parse(self.field_id).map_err(|_| ErrorCode::FieldIdIsEmpty)?;
Ok(TypeOptionPathParams {
view_id: database_id.0,
field_id: field_id.0,
field_type: self.field_type,
})
}
}
#[derive(Debug, Default, ProtoBuf)]
pub struct TypeOptionPB {
#[pb(index = 1)]
pub view_id: String,
#[pb(index = 2)]
pub field: FieldPB,
#[pb(index = 3)]
pub type_option_data: Vec<u8>,
}
/// Collection of the [FieldPB] /// Collection of the [FieldPB]
#[derive(Debug, Default, ProtoBuf)] #[derive(Debug, Default, ProtoBuf)]
pub struct RepeatedFieldPB { pub struct RepeatedFieldPB {

View File

@ -13,7 +13,7 @@ use crate::manager::DatabaseManager;
use crate::services::cell::CellBuilder; use crate::services::cell::CellBuilder;
use crate::services::field::checklist_type_option::ChecklistCellChangeset; use crate::services::field::checklist_type_option::ChecklistCellChangeset;
use crate::services::field::{ use crate::services::field::{
type_option_data_from_pb_or_default, DateCellChangeset, SelectOptionCellChangeset, type_option_data_from_pb, DateCellChangeset, SelectOptionCellChangeset,
}; };
use crate::services::field_settings::FieldSettingsChangesetParams; use crate::services::field_settings::FieldSettingsChangesetParams;
use crate::services::group::GroupChangeset; use crate::services::group::GroupChangeset;
@ -160,7 +160,7 @@ pub(crate) async fn get_fields_handler(
let fields = database_editor let fields = database_editor
.get_fields(&params.view_id, params.field_ids) .get_fields(&params.view_id, params.field_ids)
.into_iter() .into_iter()
.map(FieldPB::from) .map(FieldPB::new)
.collect::<Vec<FieldPB>>() .collect::<Vec<FieldPB>>()
.into(); .into();
data_result_ok(fields) data_result_ok(fields)
@ -178,7 +178,7 @@ pub(crate) async fn get_primary_field_handler(
.get_fields(&view_id, None) .get_fields(&view_id, None)
.into_iter() .into_iter()
.filter(|field| field.is_primary) .filter(|field| field.is_primary)
.map(FieldPB::from) .map(FieldPB::new)
.collect::<Vec<FieldPB>>(); .collect::<Vec<FieldPB>>();
if fields.is_empty() { if fields.is_empty() {
@ -217,8 +217,7 @@ pub(crate) async fn update_field_type_option_handler(
let database_editor = manager.get_database_with_view_id(&params.view_id).await?; let database_editor = manager.get_database_with_view_id(&params.view_id).await?;
if let Some(old_field) = database_editor.get_field(&params.field_id) { if let Some(old_field) = database_editor.get_field(&params.field_id) {
let field_type = FieldType::from(old_field.field_type); let field_type = FieldType::from(old_field.field_type);
let type_option_data = let type_option_data = type_option_data_from_pb(params.type_option_data, &field_type)?;
type_option_data_from_pb_or_default(params.type_option_data, &field_type);
database_editor database_editor
.update_field_type_option( .update_field_type_option(
&params.view_id, &params.view_id,
@ -293,46 +292,19 @@ pub(crate) async fn duplicate_field_handler(
Ok(()) Ok(())
} }
/// Return the FieldTypeOptionData if the Field exists otherwise return record not found error. /// Create a field and save it. Returns the [FieldPB] in the current view.
#[tracing::instrument(level = "trace", skip(data, manager), err)]
pub(crate) async fn get_field_type_option_data_handler(
data: AFPluginData<TypeOptionPathPB>,
manager: AFPluginState<Weak<DatabaseManager>>,
) -> DataResult<TypeOptionPB, FlowyError> {
let manager = upgrade_manager(manager)?;
let params: TypeOptionPathParams = data.into_inner().try_into()?;
let database_editor = manager.get_database_with_view_id(&params.view_id).await?;
if let Some((field, data)) = database_editor
.get_field_type_option_data(&params.field_id)
.await
{
let data = TypeOptionPB {
view_id: params.view_id,
field: FieldPB::from(field),
type_option_data: data.to_vec(),
};
data_result_ok(data)
} else {
Err(FlowyError::record_not_found())
}
}
/// Create TypeOptionPB and save it. Return the FieldTypeOptionData.
#[tracing::instrument(level = "trace", skip(data, manager), err)] #[tracing::instrument(level = "trace", skip(data, manager), err)]
pub(crate) async fn create_field_handler( pub(crate) async fn create_field_handler(
data: AFPluginData<CreateFieldPayloadPB>, data: AFPluginData<CreateFieldPayloadPB>,
manager: AFPluginState<Weak<DatabaseManager>>, manager: AFPluginState<Weak<DatabaseManager>>,
) -> DataResult<TypeOptionPB, FlowyError> { ) -> DataResult<FieldPB, FlowyError> {
let manager = upgrade_manager(manager)?; let manager = upgrade_manager(manager)?;
let params: CreateFieldParams = data.into_inner().try_into()?; let params: CreateFieldParams = data.into_inner().try_into()?;
let database_editor = manager.get_database_with_view_id(&params.view_id).await?; let database_editor = manager.get_database_with_view_id(&params.view_id).await?;
let (field, data) = database_editor.create_field_with_type_option(&params).await; let data = database_editor
.create_field_with_type_option(params)
.await?;
let data = TypeOptionPB {
view_id: params.view_id,
field: FieldPB::from(field),
type_option_data: data.to_vec(),
};
data_result_ok(data) data_result_ok(data)
} }

View File

@ -30,7 +30,6 @@ pub fn init(database_manager: Weak<DatabaseManager>) -> AFPlugin {
.event(DatabaseEvent::UpdateFieldType, switch_to_field_handler) .event(DatabaseEvent::UpdateFieldType, switch_to_field_handler)
.event(DatabaseEvent::DuplicateField, duplicate_field_handler) .event(DatabaseEvent::DuplicateField, duplicate_field_handler)
.event(DatabaseEvent::MoveField, move_field_handler) .event(DatabaseEvent::MoveField, move_field_handler)
.event(DatabaseEvent::GetTypeOption, get_field_type_option_data_handler)
.event(DatabaseEvent::CreateField, create_field_handler) .event(DatabaseEvent::CreateField, create_field_handler)
// Row // Row
.event(DatabaseEvent::CreateRow, create_row_handler) .event(DatabaseEvent::CreateRow, create_row_handler)
@ -174,18 +173,9 @@ pub enum DatabaseEvent {
#[event(input = "MoveFieldPayloadPB")] #[event(input = "MoveFieldPayloadPB")]
MoveField = 22, MoveField = 22,
/// [TypeOptionPathPB] event is used to get the FieldTypeOption data for a specific field type.
///
/// Check out the [TypeOptionPB] for more details. If the [FieldTypeOptionData] does exist
/// for the target type, the [TypeOptionBuilder] will create the default data for that type.
///
/// Return the [TypeOptionPB] if there are no errors.
#[event(input = "TypeOptionPathPB", output = "TypeOptionPB")]
GetTypeOption = 23,
/// [CreateField] event is used to create a new field with an optional /// [CreateField] event is used to create a new field with an optional
/// TypeOptionData. /// TypeOptionData.
#[event(input = "CreateFieldPayloadPB", output = "TypeOptionPB")] #[event(input = "CreateFieldPayloadPB", output = "FieldPB")]
CreateField = 24, CreateField = 24,
#[event(input = "DatabaseViewIdPB", output = "FieldPB")] #[event(input = "DatabaseViewIdPB", output = "FieldPB")]

View File

@ -1,7 +1,6 @@
use std::collections::HashMap; use std::collections::HashMap;
use std::sync::Arc; use std::sync::Arc;
use bytes::Bytes;
use collab_database::database::MutexDatabase; use collab_database::database::MutexDatabase;
use collab_database::fields::{Field, TypeOptionData}; use collab_database::fields::{Field, TypeOptionData};
use collab_database::rows::{Cell, Cells, CreateRowParams, Row, RowCell, RowDetail, RowId}; use collab_database::rows::{Cell, Cells, CreateRowParams, Row, RowCell, RowDetail, RowId};
@ -28,8 +27,8 @@ use crate::services::database_view::{
use crate::services::field::checklist_type_option::ChecklistCellChangeset; use crate::services::field::checklist_type_option::ChecklistCellChangeset;
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, SelectOptionCellChangeset, type_option_data_from_pb, SelectOptionCellChangeset, SelectOptionIds, TimestampCellData,
SelectOptionIds, TimestampCellData, TypeOptionCellDataHandler, TypeOptionCellExt, 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,
@ -469,28 +468,20 @@ impl DatabaseEditor {
Ok(None) Ok(None)
} }
pub async fn get_field_type_option_data(&self, field_id: &str) -> Option<(Field, Bytes)> { pub async fn create_field_with_type_option(
let field = self.database.lock().fields.get_field(field_id); &self,
field.map(|field| { params: CreateFieldParams,
let field_type = FieldType::from(field.field_type); ) -> FlowyResult<FieldPB> {
let type_option = field
.get_any_type_option(field_type)
.unwrap_or_else(|| default_type_option_data_from_type(&field_type));
(field, type_option_to_pb(type_option, &field_type))
})
}
pub async fn create_field_with_type_option(&self, params: &CreateFieldParams) -> (Field, Bytes) {
let name = params let name = params
.field_name .field_name
.clone() .clone()
.unwrap_or_else(|| params.field_type.default_name()); .unwrap_or_else(|| params.field_type.default_name());
let type_option_data = match &params.type_option_data {
None => default_type_option_data_from_type(&params.field_type), let type_option_data = params
Some(type_option_data) => { .type_option_data
type_option_data_from_pb_or_default(type_option_data.clone(), &params.field_type) .and_then(|data| type_option_data_from_pb(data, &params.field_type).ok())
}, .unwrap_or(default_type_option_data_from_type(&params.field_type));
};
let (index, field) = self.database.lock().create_field_with_mut( let (index, field) = self.database.lock().create_field_with_mut(
&params.view_id, &params.view_id,
name, name,
@ -499,7 +490,7 @@ impl DatabaseEditor {
|field| { |field| {
field field
.type_options .type_options
.insert(params.field_type.to_string(), type_option_data.clone()); .insert(params.field_type.to_string(), type_option_data);
}, },
default_field_settings_by_layout_map(), default_field_settings_by_layout_map(),
); );
@ -508,10 +499,7 @@ impl DatabaseEditor {
.notify_did_insert_database_field(field.clone(), index) .notify_did_insert_database_field(field.clone(), index)
.await; .await;
( Ok(FieldPB::new(field))
field,
type_option_to_pb(type_option_data, &params.field_type),
)
} }
pub async fn move_field(&self, params: MoveFieldParams) -> FlowyResult<()> { pub async fn move_field(&self, params: MoveFieldParams) -> FlowyResult<()> {
@ -539,7 +527,10 @@ impl DatabaseEditor {
if let Some(index) = new_index { if let Some(index) = new_index {
let delete_field = FieldIdPB::from(params.from_field_id); let delete_field = FieldIdPB::from(params.from_field_id);
let insert_field = IndexFieldPB::from_field(field, index); let insert_field = IndexFieldPB {
field: FieldPB::new(field),
index: index as i32,
};
let notified_changeset = DatabaseFieldChangesetPB { let notified_changeset = DatabaseFieldChangesetPB {
view_id: params.view_id, view_id: params.view_id,
inserted_fields: vec![insert_field], inserted_fields: vec![insert_field],
@ -1058,7 +1049,10 @@ impl DatabaseEditor {
#[tracing::instrument(level = "trace", skip_all, err)] #[tracing::instrument(level = "trace", skip_all, err)]
async fn notify_did_insert_database_field(&self, field: Field, index: usize) -> FlowyResult<()> { async fn notify_did_insert_database_field(&self, field: Field, index: usize) -> FlowyResult<()> {
let database_id = self.database.lock().get_database_id(); let database_id = self.database.lock().get_database_id();
let index_field = IndexFieldPB::from_field(field, index); let index_field = IndexFieldPB {
field: FieldPB::new(field),
index: index as i32,
};
let notified_changeset = DatabaseFieldChangesetPB::insert(&database_id, vec![index_field]); let notified_changeset = DatabaseFieldChangesetPB::insert(&database_id, vec![index_field]);
let _ = self.notify_did_update_database(notified_changeset).await; let _ = self.notify_did_update_database(notified_changeset).await;
Ok(()) Ok(())
@ -1629,7 +1623,7 @@ fn notify_did_update_database_field(
}; };
if let Some(field) = field { if let Some(field) = field {
let updated_field = FieldPB::from(field); let updated_field = FieldPB::new(field);
let notified_changeset = let notified_changeset =
DatabaseFieldChangesetPB::update(&database_id, vec![updated_field.clone()]); DatabaseFieldChangesetPB::update(&database_id, vec![updated_field.clone()]);

View File

@ -175,12 +175,12 @@ pub trait TypeOptionCellDataCompare: TypeOption {
} }
} }
pub fn type_option_data_from_pb_or_default<T: Into<Bytes>>( pub fn type_option_data_from_pb<T: Into<Bytes>>(
bytes: T, bytes: T,
field_type: &FieldType, field_type: &FieldType,
) -> TypeOptionData { ) -> Result<TypeOptionData, ProtobufError> {
let bytes = bytes.into(); let bytes = bytes.into();
let result: Result<TypeOptionData, ProtobufError> = match field_type { match field_type {
FieldType::RichText => { FieldType::RichText => {
RichTextTypeOptionPB::try_from(bytes).map(|pb| RichTextTypeOption::from(pb).into()) RichTextTypeOptionPB::try_from(bytes).map(|pb| RichTextTypeOption::from(pb).into())
}, },
@ -206,9 +206,7 @@ pub fn type_option_data_from_pb_or_default<T: Into<Bytes>>(
FieldType::Checklist => { FieldType::Checklist => {
ChecklistTypeOptionPB::try_from(bytes).map(|pb| ChecklistTypeOption::from(pb).into()) ChecklistTypeOptionPB::try_from(bytes).map(|pb| ChecklistTypeOption::from(pb).into())
}, },
}; }
result.unwrap_or_else(|_| default_type_option_data_from_type(field_type))
} }
pub fn type_option_to_pb(type_option: TypeOptionData, field_type: &FieldType) -> Bytes { pub fn type_option_to_pb(type_option: TypeOptionData, field_type: &FieldType) -> Bytes {

View File

@ -64,7 +64,7 @@ impl DatabaseFieldTest {
match script { match script {
FieldScript::CreateField { params } => { FieldScript::CreateField { params } => {
self.field_count += 1; self.field_count += 1;
self.editor.create_field_with_type_option(&params).await; let _ = self.editor.create_field_with_type_option(params).await;
let fields = self.editor.get_fields(&self.view_id, None); let fields = self.editor.get_fields(&self.view_id, None);
assert_eq!(self.field_count, fields.len()); assert_eq!(self.field_count, fields.len());
}, },