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
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
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/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-folder2/protobuf.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_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_option_editor.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/type_option/date.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/type_option/date/date_time_format.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/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/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/include_time_button.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/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/plugins/base/drag_handler.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_cal_bloc.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/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-error/errors.pb.dart';
import 'package:dartz/dartz.dart' hide State;
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/cupertino.dart';
@ -41,17 +37,6 @@ class MobileDateCellEditScreen extends StatefulWidget {
}
class _MobileDateCellEditScreenState extends State<MobileDateCellEditScreen> {
late final Future<Either<dynamic, FlowyError>> typeOptionFuture;
@override
void initState() {
super.initState();
typeOptionFuture = widget.controller.getTypeOption(
DateTypeOptionDataParser(),
);
}
@override
Widget build(BuildContext context) {
return widget.showAsFullScreen ? _buildFullScreen() : _buildNotFullScreen();
@ -64,7 +49,9 @@ class _MobileDateCellEditScreenState extends State<MobileDateCellEditScreen> {
LocaleKeys.titleBar_date.tr(),
),
),
body: _buildBody(),
body: _DateCellEditBody(
dateCellController: widget.controller,
),
);
}
@ -86,7 +73,9 @@ class _MobileDateCellEditScreenState extends State<MobileDateCellEditScreen> {
),
_buildHeader(),
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() {
const iconWidth = 30.0;
const height = 44.0;
@ -160,20 +119,16 @@ class _MobileDateCellEditScreenState extends State<MobileDateCellEditScreen> {
class _DateCellEditBody extends StatelessWidget {
const _DateCellEditBody({
required this.dateCellController,
required this.dateTypeOptionPB,
});
final DateCellController dateCellController;
final DateTypeOptionPB dateTypeOptionPB;
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => DateCellCalendarBloc(
dateTypeOptionPB: dateTypeOptionPB,
cellData: dateCellController.getCellData(),
create: (context) => DateCellEditorBloc(
cellController: dateCellController,
)..add(const DateCellCalendarEvent.initial()),
)..add(const DateCellEditorEvent.initial()),
child: const Column(
children: [
FlowyOptionDecorateBox(
@ -217,7 +172,7 @@ class _IncludeTimePickerState extends State<_IncludeTimePicker> {
@override
Widget build(BuildContext context) {
return BlocBuilder<DateCellCalendarBloc, DateCellCalendarState>(
return BlocBuilder<DateCellEditorBloc, DateCellEditorState>(
builder: (context, state) {
final startDay = state.dateStr;
final endDay = state.endDateStr;
@ -306,7 +261,7 @@ class _IncludeTimePickerState extends State<_IncludeTimePicker> {
return GestureDetector(
onTap: () async {
final bloc = context.read<DateCellCalendarBloc>();
final bloc = context.read<DateCellEditorBloc>();
await showMobileBottomSheet(
context,
builder: (context) {
@ -331,8 +286,8 @@ class _IncludeTimePickerState extends State<_IncludeTimePicker> {
if (_selectedTime != null) {
bloc.add(
isStartDay
? DateCellCalendarEvent.setTime(_selectedTime!)
: DateCellCalendarEvent.setEndTime(_selectedTime!),
? DateCellEditorEvent.setTime(_selectedTime!)
: DateCellEditorEvent.setEndTime(_selectedTime!),
);
}
},
@ -361,7 +316,7 @@ class _EndDateSwitch extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocSelector<DateCellCalendarBloc, DateCellCalendarState, bool>(
return BlocSelector<DateCellEditorBloc, DateCellEditorState, bool>(
selector: (state) => state.isRange,
builder: (context, isRange) {
return FlowyOptionTile.toggle(
@ -369,8 +324,8 @@ class _EndDateSwitch extends StatelessWidget {
isSelected: isRange,
onValueChanged: (value) {
context
.read<DateCellCalendarBloc>()
.add(DateCellCalendarEvent.setIsRange(value));
.read<DateCellEditorBloc>()
.add(DateCellEditorEvent.setIsRange(value));
},
);
},
@ -383,7 +338,7 @@ class _IncludeTimeSwitch extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocSelector<DateCellCalendarBloc, DateCellCalendarState, bool>(
return BlocSelector<DateCellEditorBloc, DateCellEditorState, bool>(
selector: (state) => state.includeTime,
builder: (context, includeTime) {
return FlowyOptionTile.toggle(
@ -392,8 +347,8 @@ class _IncludeTimeSwitch extends StatelessWidget {
isSelected: includeTime,
onValueChanged: (value) {
context
.read<DateCellCalendarBloc>()
.add(DateCellCalendarEvent.setIncludeTime(value));
.read<DateCellEditorBloc>()
.add(DateCellEditorEvent.setIncludeTime(value));
},
);
},
@ -420,7 +375,7 @@ class _TimeTextFieldState extends State<_TimeTextField> {
@override
Widget build(BuildContext context) {
return BlocConsumer<DateCellCalendarBloc, DateCellCalendarState>(
return BlocConsumer<DateCellEditorBloc, DateCellEditorState>(
listener: (context, state) {
_textController.text =
widget.isEndTime ? state.endTimeStr ?? "" : state.timeStr ?? "";
@ -437,10 +392,10 @@ class _TimeTextFieldState extends State<_TimeTextField> {
),
keyboardType: TextInputType.datetime,
onFieldSubmitted: (timeStr) {
context.read<DateCellCalendarBloc>().add(
context.read<DateCellEditorBloc>().add(
widget.isEndTime
? DateCellCalendarEvent.setEndTime(timeStr)
: DateCellCalendarEvent.setTime(timeStr),
? DateCellEditorEvent.setEndTime(timeStr)
: DateCellEditorEvent.setTime(timeStr),
);
},
);
@ -463,8 +418,8 @@ class _ClearDateButton extends StatelessWidget {
return FlowyOptionTile.text(
text: LocaleKeys.grid_field_clearDate.tr(),
onTap: () => context
.read<DateCellCalendarBloc>()
.add(const DateCellCalendarEvent.clearDate()),
.read<DateCellEditorBloc>()
.add(const DateCellEditorEvent.clearDate()),
);
}
}

View File

@ -28,19 +28,12 @@ class MobileEditPropertyScreen extends StatefulWidget {
}
class _MobileEditPropertyScreenState extends State<MobileEditPropertyScreen> {
late Future<FieldOptionValues?> future;
FieldOptionValues? optionValues;
late FieldOptionValues optionValues;
@override
void initState() {
super.initState();
future = FieldOptionValues.get(
viewId: widget.viewId,
fieldId: widget.field.id,
fieldType: widget.field.fieldType,
);
optionValues = FieldOptionValues.fromField(field: widget.field);
}
@override
@ -66,39 +59,30 @@ class _MobileEditPropertyScreenState extends State<MobileEditPropertyScreen> {
),
],
),
body: FutureBuilder<FieldOptionValues?>(
future: future,
builder: (context, snapshot) {
final optionValues = snapshot.data;
if (optionValues == null) {
return const Center(child: CircularProgressIndicator.adaptive());
}
return FieldOptionEditor(
mode: FieldOptionMode.edit,
isPrimary: widget.field.isPrimary,
defaultValues: optionValues,
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();
},
body: FieldOptionEditor(
mode: FieldOptionMode.edit,
isPrimary: widget.field.isPrimary,
defaultValues: optionValues,
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();
},
),
);

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

View File

@ -1,4 +1,5 @@
import 'dart:async';
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/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:equatable/equatable.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_service.dart';
@ -26,7 +27,6 @@ class CellController<T, D> extends Equatable {
DatabaseCellContext _cellContext;
final CellMemCache _cellCache;
final CellCacheKey _cacheKey;
final FieldBackendService _fieldBackendSvc;
final CellDataLoader<T> _cellDataLoader;
final CellDataPersistence<D> _cellDataPersistence;
@ -63,10 +63,6 @@ class CellController<T, D> extends Equatable {
_cellDataPersistence = cellDataPersistence,
_rowMetaListener = RowMetaListener(cellContext.rowId),
_fieldListener = SingleFieldListener(fieldId: cellContext.fieldId),
_fieldBackendSvc = FieldBackendService(
viewId: cellContext.viewId,
fieldId: cellContext.fieldInfo.id,
),
_cacheKey = CellCacheKey(
rowId: cellContext.rowId,
fieldId: cellContext.fieldInfo.id,
@ -97,6 +93,9 @@ class CellController<T, D> extends Equatable {
/// For example:
/// 12 -> $12
if (_cellDataLoader.reloadOnFieldChanged) {
_cellContext = _cellContext.copyWith(
fieldInfo: _cellContext.fieldInfo.copyWith(field: fieldPB),
);
_loadData();
}
_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].
/// [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,
) {
return _fieldBackendSvc
.getFieldTypeOptionData(fieldType: fieldType)
.then((result) {
return result.fold(
(data) => left(parser.fromBuffer(data.typeOptionData)),
(err) => right(err),
);
});
return parser.fromBuffer(_cellContext.fieldInfo.field.typeOptionData);
}
/// 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_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/field_settings_entities.pbenum.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:dartz/dartz.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
@ -11,32 +12,24 @@ import 'field_controller.dart';
import 'field_info.dart';
import 'field_listener.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';
class FieldEditorBloc extends Bloc<FieldEditorEvent, FieldEditorState> {
final FieldPB field;
final String viewId;
final String fieldId;
final FieldController fieldController;
final SingleFieldListener _singleFieldListener;
final FieldBackendService fieldService;
final FieldSettingsBackendService fieldSettingsService;
final TypeOptionController typeOptionController;
final void Function(String newFieldId)? onFieldInserted;
FieldEditorBloc({
required this.viewId,
required this.field,
required this.fieldController,
this.onFieldInserted,
required FieldTypeOptionLoader loader,
}) : typeOptionController = TypeOptionController(
field: field,
loader: loader,
),
required FieldPB field,
}) : fieldId = field.id,
_singleFieldListener = SingleFieldListener(fieldId: field.id),
fieldService = FieldBackendService(
viewId: viewId,
@ -48,12 +41,6 @@ class FieldEditorBloc extends Bloc<FieldEditorEvent, FieldEditorState> {
(event, emit) async {
await event.when(
initial: () async {
final fieldId = field.id;
typeOptionController.addFieldListener((field) {
if (!isClosed) {
add(FieldEditorEvent.didReceiveFieldChanged(fieldId));
}
});
_singleFieldListener.start(
onFieldChanged: (field) {
if (!isClosed) {
@ -61,7 +48,6 @@ class FieldEditorBloc extends Bloc<FieldEditorEvent, FieldEditorState> {
}
},
);
await typeOptionController.reloadTypeOption();
add(FieldEditorEvent.didReceiveFieldChanged(fieldId));
},
didReceiveFieldChanged: (fieldId) async {
@ -69,23 +55,31 @@ class FieldEditorBloc extends Bloc<FieldEditorEvent, FieldEditorState> {
emit(state.copyWith(field: fieldController.getField(fieldId)!));
},
switchFieldType: (fieldType) async {
await typeOptionController.switchToField(fieldType);
await fieldService.updateFieldType(fieldType: fieldType);
},
renameField: (newName) async {
final result = await fieldService.updateField(name: newName);
_logIfError(result);
},
updateTypeOption: (typeOptionData) async {
final result = await FieldBackendService.updateFieldTypeOption(
viewId: viewId,
fieldId: fieldId,
typeOptionData: typeOptionData,
);
_logIfError(result);
},
insertLeft: () async {
final result = await fieldService.insertBefore();
result.fold(
(typeOptionPB) => onFieldInserted?.call(typeOptionPB.field_2.id),
(newField) => onFieldInserted?.call(newField.id),
(err) => Log.error("Failed creating field $err"),
);
},
insertRight: () async {
final result = await fieldService.insertAfter();
result.fold(
(typeOptionPB) => onFieldInserted?.call(typeOptionPB.field_2.id),
(newField) => onFieldInserted?.call(newField.id),
(err) => Log.error("Failed creating field $err"),
);
},
@ -129,6 +123,9 @@ class FieldEditorEvent with _$FieldEditorEvent {
_DidReceiveFieldChanged;
const factory FieldEditorEvent.switchFieldType(final FieldType fieldType) =
_SwitchFieldType;
const factory FieldEditorEvent.updateTypeOption(
final Uint8List typeOptionData,
) = _UpdateTypeOption;
const factory FieldEditorEvent.renameField(final String name) = _RenameField;
const factory FieldEditorEvent.insertLeft() = _InsertLeft;
const factory FieldEditorEvent.insertRight() = _InsertRight;

View File

@ -15,7 +15,7 @@ class FieldBackendService {
final String viewId;
final String fieldId;
static Future<Either<TypeOptionPB, FlowyError>> createField({
static Future<Either<FieldPB, FlowyError>> createField({
required String viewId,
FieldType fieldType = FieldType.RichText,
String? fieldName,
@ -33,7 +33,7 @@ class FieldBackendService {
return DatabaseEventCreateField(payload).send();
}
Future<Either<TypeOptionPB, FlowyError>> insertBefore({
Future<Either<FieldPB, FlowyError>> insertBefore({
FieldType fieldType = FieldType.RichText,
String? fieldName,
Uint8List? typeOptionData,
@ -50,7 +50,7 @@ class FieldBackendService {
);
}
Future<Either<TypeOptionPB, FlowyError>> insertAfter({
Future<Either<FieldPB, FlowyError>> insertAfter({
FieldType fieldType = FieldType.RichText,
String? fieldName,
Uint8List? typeOptionData,
@ -61,7 +61,7 @@ class FieldBackendService {
fieldName: fieldName,
typeOptionData: typeOptionData,
position: OrderObjectPositionPB(
position: OrderObjectPositionTypePB.Before,
position: OrderObjectPositionTypePB.After,
objectId: fieldId,
),
);
@ -156,21 +156,6 @@ class FieldBackendService {
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.
static Future<Either<FieldPB, FlowyError>> getPrimaryField({
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/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 {
final String viewId;
final String fieldId;
import 'select_option_type_option_bloc.dart';
import 'type_option_service.dart';
class MultiSelectAction implements ISelectOptionAction {
final TypeOptionBackendService service;
final MultiSelectTypeOptionContext typeOptionContext;
final TypeOptionDataCallback onTypeOptionUpdated;
MultiSelectAction({
required this.viewId,
required this.fieldId,
required this.typeOptionContext,
}) : service = TypeOptionBackendService(
viewId: viewId,
fieldId: fieldId,
);
required this.onTypeOptionUpdated,
required String viewId,
required String fieldId,
}) : service = TypeOptionBackendService(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) {
typeOptionContext.typeOption = newTypeOption;
_updateTypeOption(newOptions);
return newOptions;
},
(err) {
Log.error(err);
return newOptions;
},
);
});
}
@override
List<SelectOptionPB> Function(SelectOptionPB) get deleteOption {
return (SelectOptionPB option) {
typeOption.freeze();
typeOption = typeOption.rebuild((typeOption) {
final index =
typeOption.options.indexWhere((element) => element.id == option.id);
if (index != -1) {
typeOption.options.removeAt(index);
}
});
return typeOption.options;
};
List<SelectOptionPB> deleteOption(
List<SelectOptionPB> options,
SelectOptionPB deletedOption,
) {
final newOptions = List<SelectOptionPB>.from(options);
final index =
newOptions.indexWhere((option) => option.id == deletedOption.id);
if (index != -1) {
newOptions.removeAt(index);
}
_updateTypeOption(newOptions);
return newOptions;
}
@override
Future<List<SelectOptionPB>> Function(String) get insertOption {
return (String optionName) {
return service.newOption(name: optionName).then((result) {
return result.fold(
(option) {
typeOption.freeze();
typeOption = typeOption.rebuild((typeOption) {
final exists = typeOption.options
.any((element) => element.name == option.name);
if (!exists) {
typeOption.options.insert(0, option);
}
});
List<SelectOptionPB> updateOption(
List<SelectOptionPB> options,
SelectOptionPB option,
) {
final newOptions = List<SelectOptionPB>.from(options);
final index = newOptions.indexWhere((element) => element.id == option.id);
if (index != -1) {
newOptions[index] = option;
}
return typeOption.options;
},
(err) {
Log.error(err);
return typeOption.options;
},
);
});
};
_updateTypeOption(newOptions);
return newOptions;
}
@override
List<SelectOptionPB> Function(SelectOptionPB) get updateOption {
return (SelectOptionPB option) {
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;
};
void _updateTypeOption(List<SelectOptionPB> options) {
final newTypeOption = MultiSelectTypeOptionPB()..options.addAll(options);
onTypeOptionUpdated(newTypeOption.writeToBuffer());
}
}

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';
part 'select_option_type_option_bloc.freezed.dart';
abstract mixin class ISelectOptionAction {
Future<List<SelectOptionPB>> Function(String) get insertOption;
abstract class ISelectOptionAction {
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
@ -26,7 +35,7 @@ class SelectOptionTypeOptionBloc
await event.when(
createOption: (optionName) async {
final List<SelectOptionPB> options =
await typeOptionAction.insertOption(optionName);
await typeOptionAction.insertOption(state.options, optionName);
emit(state.copyWith(options: options));
},
addingOption: () {
@ -37,12 +46,12 @@ class SelectOptionTypeOptionBloc
},
updateOption: (option) {
final List<SelectOptionPB> options =
typeOptionAction.updateOption(option);
typeOptionAction.updateOption(state.options, option);
emit(state.copyWith(options: options));
},
deleteOption: (option) {
final List<SelectOptionPB> options =
typeOptionAction.deleteOption(option);
typeOptionAction.deleteOption(state.options, option);
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/protobuf/flowy-database2/select_option.pb.dart';
import 'dart:async';
import 'package:protobuf/protobuf.dart';
import 'select_option_type_option_bloc.dart';
import 'type_option_context.dart';
import 'type_option_service.dart';
class SingleSelectAction with ISelectOptionAction {
final String viewId;
final String fieldId;
final SingleSelectTypeOptionContext typeOptionContext;
class SingleSelectAction implements ISelectOptionAction {
final TypeOptionBackendService service;
final TypeOptionDataCallback onTypeOptionUpdated;
SingleSelectAction({
required this.viewId,
required this.fieldId,
required this.typeOptionContext,
required this.onTypeOptionUpdated,
required String viewId,
required String 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) {
typeOptionContext.typeOption = newTypeOption;
_updateTypeOption(newOptions);
return newOptions;
},
(err) {
Log.error(err);
return newOptions;
},
);
});
}
@override
List<SelectOptionPB> Function(SelectOptionPB) get deleteOption {
return (SelectOptionPB option) {
typeOption.freeze();
typeOption = typeOption.rebuild((typeOption) {
final index =
typeOption.options.indexWhere((element) => element.id == option.id);
if (index != -1) {
typeOption.options.removeAt(index);
}
});
return typeOption.options;
};
List<SelectOptionPB> deleteOption(
List<SelectOptionPB> options,
SelectOptionPB deletedOption,
) {
final newOptions = List<SelectOptionPB>.from(options);
final index =
newOptions.indexWhere((option) => option.id == deletedOption.id);
if (index != -1) {
newOptions.removeAt(index);
}
final newTypeOption = MultiSelectTypeOptionPB()..options.addAll(newOptions);
onTypeOptionUpdated(newTypeOption.writeToBuffer());
return newOptions;
}
@override
Future<List<SelectOptionPB>> Function(String) get insertOption {
return (String optionName) {
return service.newOption(name: optionName).then((result) {
return result.fold(
(option) {
typeOption.freeze();
typeOption = typeOption.rebuild((typeOption) {
final exists = typeOption.options
.any((element) => element.name == option.name);
if (!exists) {
typeOption.options.insert(0, option);
}
});
List<SelectOptionPB> updateOption(
List<SelectOptionPB> options,
SelectOptionPB updatedOption,
) {
final newOptions = List<SelectOptionPB>.from(options);
final index =
newOptions.indexWhere((option) => option.id == updatedOption.id);
if (index != -1) {
newOptions[index] = updatedOption;
}
return typeOption.options;
},
(err) {
Log.error(err);
return typeOption.options;
},
);
});
};
_updateTypeOption(newOptions);
return newOptions;
}
@override
List<SelectOptionPB> Function(SelectOptionPB) get updateOption {
return (SelectOptionPB option) {
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;
};
void _updateTypeOption(List<SelectOptionPB> options) {
final newTypeOption = SingleSelectTypeOptionPB()..options.addAll(options);
onTypeOptionUpdated(newTypeOption.writeToBuffer());
}
}

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() {
delegate.loadOptions().then((options) {
if (!isClosed) {
String filterDesc = '';
for (final option in options) {
if (state.filter.optionIds.contains(option.id)) {
filterDesc += "${option.name} ";
}
if (!isClosed) {
final options = delegate.loadOptions();
String filterDesc = '';
for (final option in options) {
if (state.filter.optionIds.contains(option.id)) {
filterDesc += "${option.name} ";
}
add(SelectOptionFilterEditorEvent.updateFilterDescription(filterDesc));
}
});
add(SelectOptionFilterEditorEvent.updateFilterDescription(filterDesc));
}
}
@override

View File

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

View File

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

View File

@ -31,12 +31,14 @@ class _SelectOptionFilterChoicechipState
if (widget.filterInfo.fieldInfo.fieldType == FieldType.SingleSelect) {
bloc = SelectOptionFilterEditorBloc(
filterInfo: widget.filterInfo,
delegate: SingleSelectOptionFilterDelegateImpl(widget.filterInfo),
delegate:
SingleSelectOptionFilterDelegateImpl(filterInfo: widget.filterInfo),
);
} else {
bloc = SelectOptionFilterEditorBloc(
filterInfo: widget.filterInfo,
delegate: MultiSelectOptionFilterDelegateImpl(widget.filterInfo),
delegate:
MultiSelectOptionFilterDelegateImpl(filterInfo: widget.filterInfo),
);
}
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/grid/presentation/widgets/header/type_option/builder.dart';
import 'package:appflowy_backend/log.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/filter/filter_info.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/select_option.pb.dart';
import '../../filter_info.dart';
abstract class SelectOptionFilterDelegate {
Future<List<SelectOptionPB>> loadOptions();
List<SelectOptionPB> loadOptions();
}
class SingleSelectOptionFilterDelegateImpl
implements SelectOptionFilterDelegate {
final SingleSelectTypeOptionContext typeOptionContext;
final FilterInfo filterInfo;
SingleSelectOptionFilterDelegateImpl(FilterInfo filterInfo)
: typeOptionContext = makeSingleSelectTypeOptionContext(
viewId: filterInfo.viewId,
fieldPB: filterInfo.fieldInfo.field,
);
SingleSelectOptionFilterDelegateImpl({
required this.filterInfo,
});
@override
Future<List<SelectOptionPB>> loadOptions() {
return typeOptionContext
.loadTypeOptionData(
onError: (error) => Log.error(error),
)
.then((value) => value.options);
List<SelectOptionPB> loadOptions() {
final parser = SingleSelectTypeOptionDataParser();
return parser.fromBuffer(filterInfo.fieldInfo.field.typeOptionData).options;
}
}
class MultiSelectOptionFilterDelegateImpl
implements SelectOptionFilterDelegate {
final MultiSelectTypeOptionContext typeOptionContext;
final FilterInfo filterInfo;
MultiSelectOptionFilterDelegateImpl(FilterInfo filterInfo)
: typeOptionContext = makeMultiSelectTypeOptionContext(
viewId: filterInfo.viewId,
fieldPB: filterInfo.fieldInfo.field,
);
MultiSelectOptionFilterDelegateImpl({required this.filterInfo});
@override
Future<List<SelectOptionPB>> loadOptions() {
return typeOptionContext
.loadTypeOptionData(
onError: (error) => Log.error(error),
)
.then((value) => value.options);
List<SelectOptionPB> loadOptions() {
final parser = MultiSelectTypeOptionDataParser();
return parser.fromBuffer(filterInfo.fieldInfo.field.typeOptionData).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/locale_keys.g.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_info.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/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_backend/protobuf/flowy-database2/field_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:styled_widget/styled_widget.dart';
import 'field_type_option_editor.dart';
import 'field_type_list.dart';
import 'type_option/builder.dart';
enum FieldEditorPage {
general,
@ -70,10 +73,6 @@ class _FieldEditorState extends State<FieldEditor> {
field: widget.field,
fieldController: widget.fieldController,
onFieldInserted: widget.onFieldInserted,
loader: FieldTypeOptionLoader(
viewId: widget.viewId,
field: widget.field,
),
)..add(const FieldEditorEvent.initial()),
child: _currentPage == FieldEditorPage.details
? _fieldDetails()
@ -326,12 +325,13 @@ class _FieldDetailsEditorState extends State<FieldDetailsEditor> {
final List<Widget> children = [
FieldNameTextField(
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,
),
const VSpace(8),
FieldTypeOptionCell(popoverMutex: popoverMutex),
const TypeOptionSeparator(),
const VSpace(8.0),
SwitchFieldButton(popoverMutex: popoverMutex),
const TypeOptionSeparator(spacing: 8.0),
FieldTypeOptionEditor(viewId: widget.viewId, popoverMutex: popoverMutex),
_addFieldVisibilityToggleButton(),
_addDuplicateFieldButton(),
_addDeleteFieldButton(),
@ -350,7 +350,7 @@ class _FieldDetailsEditorState extends State<FieldDetailsEditor> {
return BlocBuilder<FieldEditorBloc, FieldEditorState>(
builder: (context, state) {
return Padding(
padding: const EdgeInsets.fromLTRB(8.0, 2.0, 8.0, 0),
padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: FieldActionCell(
viewId: widget.viewId,
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;
const FieldTypeOptionCell({
const FieldTypeOptionEditor({
super.key,
required this.viewId,
required this.popoverMutex,
});
@ -416,14 +418,28 @@ class FieldTypeOptionCell extends StatelessWidget {
if (state.field.isPrimary) {
return const SizedBox.shrink();
}
final dataController =
context.read<FieldEditorBloc>().typeOptionController;
return Padding(
padding: const EdgeInsets.only(bottom: 2.0),
child: FieldTypeOptionEditor(
dataController: dataController,
popoverMutex: popoverMutex,
),
final typeOptionEditor = makeTypeOptionEditor(
context: context,
viewId: viewId,
field: state.field.field,
popoverMutex: popoverMutex,
onTypeOptionUpdated: (Uint8List typeOptionData) {
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();
}
}
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,
);
result.fold(
(typeOptionPB) => widget.onFieldCreated(typeOptionPB.field_2.id),
(field) => widget.onFieldCreated(field.id),
(err) => Log.error("Failed to create field type option: $err"),
);
},

View File

@ -1,19 +1,9 @@
import 'dart:typed_data';
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/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_backend/protobuf/flowy-database2/protobuf.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 'checkbox.dart';
import 'checklist.dart';
import 'date.dart';
@ -24,237 +14,47 @@ import 'single_select.dart';
import 'timestamp.dart';
import 'url.dart';
typedef TypeOptionData = Uint8List;
typedef TypeOptionDataCallback = void Function(TypeOptionData typeOptionData);
typedef ShowOverlayCallback = void Function(
BuildContext anchorContext,
Widget child, {
VoidCallback? onRemoved,
});
typedef HideOverlayCallback = void Function(BuildContext anchorContext);
typedef TypeOptionDataCallback = void Function(Uint8List typeOptionData);
class TypeOptionOverlayDelegate {
ShowOverlayCallback showOverlay;
HideOverlayCallback hideOverlay;
TypeOptionOverlayDelegate({
required this.showOverlay,
required this.hideOverlay,
abstract class TypeOptionEditorFactory {
Widget? build({
required BuildContext context,
required String viewId,
required FieldPB field,
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? build(BuildContext context);
}
Widget? makeTypeOptionWidget({
Widget? makeTypeOptionEditor({
required BuildContext context,
required TypeOptionController dataController,
required String viewId,
required FieldPB field,
required PopoverMutex popoverMutex,
required TypeOptionDataCallback onTypeOptionUpdated,
}) {
final builder = makeTypeOptionWidgetBuilder(
dataController: dataController,
final editorBuilder = TypeOptionEditorFactory.makeBuilder(field.fieldType);
return editorBuilder.build(
context: context,
viewId: viewId,
field: field,
onTypeOptionUpdated: onTypeOptionUpdated,
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 'builder.dart';
class CheckboxTypeOptionWidgetBuilder extends TypeOptionWidgetBuilder {
CheckboxTypeOptionWidgetBuilder(CheckboxTypeOptionContext typeOptionContext);
class CheckboxTypeOptionEditorFactory implements TypeOptionEditorFactory {
const CheckboxTypeOptionEditorFactory();
@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 'builder.dart';
class ChecklistTypeOptionWidgetBuilder extends TypeOptionWidgetBuilder {
ChecklistTypeOptionWidgetBuilder(
ChecklistTypeOptionContext typeOptionContext,
);
class ChecklistTypeOptionEditorFactory implements TypeOptionEditorFactory {
const ChecklistTypeOptionEditorFactory();
@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/generated/locale_keys.g.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/plugins/database_view/application/field/type_option/type_option_data_parser.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.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:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:protobuf/protobuf.dart';
import '../../../layout/sizes.dart';
import '../../common/type_option_separator.dart';
import '../field_type_option_editor.dart';
import 'builder.dart';
import 'date/date_time_format.dart';
class DateTypeOptionWidgetBuilder extends TypeOptionWidgetBuilder {
final DateTypeOptionWidget _widget;
DateTypeOptionWidgetBuilder(
DateTypeOptionContext typeOptionContext,
PopoverMutex popoverMutex,
) : _widget = DateTypeOptionWidget(
typeOptionContext: typeOptionContext,
popoverMutex: popoverMutex,
);
class DateTypeOptionEditorFactory implements TypeOptionEditorFactory {
const DateTypeOptionEditorFactory();
@override
Widget? build(BuildContext context) {
return _widget;
}
}
Widget? build({
required BuildContext context,
required String viewId,
required FieldPB field,
required PopoverMutex popoverMutex,
required TypeOptionDataCallback onTypeOptionUpdated,
}) {
final typeOption = _parseTypeOptionData(field.typeOptionData);
class DateTypeOptionWidget extends TypeOptionWidget {
final DateTypeOptionContext typeOptionContext;
final PopoverMutex popoverMutex;
const DateTypeOptionWidget({
required this.typeOptionContext,
required this.popoverMutex,
super.key,
});
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) =>
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],
);
},
),
return Column(
mainAxisSize: MainAxisSize.min,
children: [
_renderDateFormatButton(
typeOption,
popoverMutex,
onTypeOptionUpdated,
),
VSpace(GridSize.typeOptionSeparatorHeight),
_renderTimeFormatButton(
typeOption,
popoverMutex,
onTypeOptionUpdated,
),
],
);
}
Widget _renderDateFormatButton(
BuildContext context,
DateFormatPB dataFormat,
DateTypeOptionPB typeOption,
PopoverMutex popoverMutex,
TypeOptionDataCallback onTypeOptionUpdated,
) {
return AppFlowyPopover(
mutex: popoverMutex,
@ -84,11 +53,11 @@ class DateTypeOptionWidget extends TypeOptionWidget {
constraints: BoxConstraints.loose(const Size(460, 440)),
popupBuilder: (popoverContext) {
return DateFormatList(
selectedFormat: dataFormat,
selectedFormat: typeOption.dateFormat,
onSelected: (format) {
context
.read<DateTypeOptionBloc>()
.add(DateTypeOptionEvent.didSelectDateFormat(format));
final newTypeOption =
_updateTypeOption(typeOption: typeOption, dateFormat: format);
onTypeOptionUpdated(newTypeOption.writeToBuffer());
PopoverContainer.of(popoverContext).close();
},
);
@ -101,8 +70,9 @@ class DateTypeOptionWidget extends TypeOptionWidget {
}
Widget _renderTimeFormatButton(
BuildContext context,
TimeFormatPB timeFormat,
DateTypeOptionPB typeOption,
PopoverMutex popoverMutex,
TypeOptionDataCallback onTypeOptionUpdated,
) {
return AppFlowyPopover(
mutex: popoverMutex,
@ -112,227 +82,40 @@ class DateTypeOptionWidget extends TypeOptionWidget {
constraints: BoxConstraints.loose(const Size(460, 440)),
popupBuilder: (BuildContext popoverContext) {
return TimeFormatList(
selectedFormat: timeFormat,
selectedFormat: typeOption.timeFormat,
onSelected: (format) {
context
.read<DateTypeOptionBloc>()
.add(DateTypeOptionEvent.didSelectTimeFormat(format));
final newTypeOption =
_updateTypeOption(typeOption: typeOption, timeFormat: format);
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(),
),
);
}
}
class DateFormatButton extends StatelessWidget {
final VoidCallback? onTap;
final void Function(bool)? onHover;
const DateFormatButton({
this.onTap,
this.onHover,
super.key,
});
DateTypeOptionPB _parseTypeOptionData(List<int> data) {
return DateTypeOptionDataParser().fromBuffer(data);
}
@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 {
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;
}
DateTypeOptionPB _updateTypeOption({
required DateTypeOptionPB typeOption,
DateFormatPB? dateFormat,
TimeFormatPB? timeFormat,
}) {
typeOption.freeze();
return typeOption.rebuild((typeOption) {
if (dateFormat != null) {
typeOption.dateFormat = dateFormat;
}
if (timeFormat != null) {
typeOption.timeFormat = timeFormat;
}
});
}
}

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/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:appflowy_popover/appflowy_popover.dart';
import '../field_type_option_editor.dart';
import 'builder.dart';
import 'select_option.dart';
import 'select/select_option.dart';
class MultiSelectTypeOptionWidgetBuilder extends TypeOptionWidgetBuilder {
final MultiSelectTypeOptionWidget _widget;
MultiSelectTypeOptionWidgetBuilder(
MultiSelectTypeOptionContext typeOptionContext,
PopoverMutex popoverMutex,
) : _widget = MultiSelectTypeOptionWidget(
selectOptionAction: MultiSelectAction(
fieldId: typeOptionContext.fieldId,
viewId: typeOptionContext.viewId,
typeOptionContext: typeOptionContext,
),
popoverMutex: popoverMutex,
);
class MultiSelectTypeOptionEditorFactory implements TypeOptionEditorFactory {
const MultiSelectTypeOptionEditorFactory();
@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(
options: selectOptionAction.typeOption.options,
beginEdit: () {
PopoverContainer.of(context).closeAll();
},
options: typeOption.options,
beginEdit: () => PopoverContainer.of(context).closeAll(),
popoverMutex: popoverMutex,
typeOptionAction: selectOptionAction,
// key: ValueKey(state.typeOption.hashCode),
typeOptionAction: MultiSelectAction(
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/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/type_option_context.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/number_entities.pbenum.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:appflowy_popover/appflowy_popover.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:protobuf/protobuf.dart';
import '../../../layout/sizes.dart';
import '../../common/type_option_separator.dart';
import '../field_type_option_editor.dart';
import 'builder.dart';
class NumberTypeOptionWidgetBuilder extends TypeOptionWidgetBuilder {
final NumberTypeOptionWidget _widget;
NumberTypeOptionWidgetBuilder(
NumberTypeOptionContext typeOptionContext,
PopoverMutex popoverMutex,
) : _widget = NumberTypeOptionWidget(
typeOptionContext: typeOptionContext,
popoverMutex: popoverMutex,
);
class NumberTypeOptionEditorFactory implements TypeOptionEditorFactory {
const NumberTypeOptionEditorFactory();
@override
Widget? build(BuildContext context) {
return Column(
children: [
VSpace(GridSize.typeOptionSeparatorHeight),
const TypeOptionSeparator(),
_widget,
],
Widget? build({
required BuildContext context,
required String viewId,
required FieldPB field,
required PopoverMutex popoverMutex,
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 NumberTypeOptionContext typeOptionContext;
final PopoverMutex popoverMutex;
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,
),
);
const NumberTypeOptionWidget({
super.key,
required this.typeOptionContext,
required this.popoverMutex,
});
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) =>
NumberTypeOptionBloc(typeOptionContext: typeOptionContext),
child: BlocConsumer<NumberTypeOptionBloc, NumberTypeOptionState>(
listener: (context, state) =>
typeOptionContext.typeOption = state.typeOption,
builder: (context, state) {
final selectNumUnitButton = SizedBox(
height: GridSize.popoverItemHeight,
child: FlowyButton(
rightIcon: const FlowySvg(FlowySvgs.more_s),
text: FlowyText.regular(
state.typeOption.format.title(),
),
),
);
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,
);
},
),
],
),
);
},
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(
selectedFormat: typeOption.format,
onSelected: (format) {
final newTypeOption = _updateNumberFormat(typeOption, format);
onTypeOptionUpdated(newTypeOption.writeToBuffer());
PopoverContainer.of(popoverContext).close();
},
);
},
),
],
),
);
}
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 {
final SelectNumberFormatCallback onSelected;
final NumberFormatPB selectedFormat;
const NumberFormatList({
super.key,
required this.selectedFormat,
required this.onSelected,
super.key,
});
@override
@ -171,7 +150,7 @@ class NumberFormatList extends StatelessWidget {
class NumberFormatCell extends StatelessWidget {
final NumberFormatPB format;
final bool isSelected;
final Function(NumberFormatPB format) onSelected;
final SelectNumberFormatCallback onSelected;
const NumberFormatCell({
required this.isSelected,
required this.format,
@ -181,10 +160,7 @@ class NumberFormatCell extends StatelessWidget {
@override
Widget build(BuildContext context) {
Widget? checkmark;
if (isSelected) {
checkmark = const FlowySvg(FlowySvgs.check_s);
}
final checkmark = isSelected ? const FlowySvg(FlowySvgs.check_s) : null;
return SizedBox(
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 'builder.dart';
class RichTextTypeOptionWidgetBuilder extends TypeOptionWidgetBuilder {
RichTextTypeOptionWidgetBuilder(RichTextTypeOptionContext typeOptionContext);
class RichTextTypeOptionEditorFactory implements TypeOptionEditorFactory {
const RichTextTypeOptionEditorFactory();
@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/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_popover/appflowy_popover.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:easy_localization/easy_localization.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';
class SelectOptionTypeOptionWidget extends StatelessWidget {
@ -39,7 +37,6 @@ class SelectOptionTypeOptionWidget extends StatelessWidget {
BlocBuilder<SelectOptionTypeOptionBloc, SelectOptionTypeOptionState>(
builder: (context, state) {
final List<Widget> children = [
const TypeOptionSeparator(spacing: 8),
const _OptionTitle(),
const VSpace(4),
if (state.isEditingOption) ...[
@ -127,13 +124,7 @@ class _OptionCell extends StatefulWidget {
}
class _OptionCellState extends State<_OptionCell> {
late PopoverController _popoverController;
@override
void initState() {
_popoverController = PopoverController();
super.initState();
}
final PopoverController _popoverController = PopoverController();
@override
Widget build(BuildContext context) {
@ -230,18 +221,18 @@ class _CreateOptionTextFieldState extends State<CreateOptionTextField> {
@override
void initState() {
_focusNode = FocusNode();
_focusNode.addListener(() {
if (_focusNode.hasFocus) {
widget.popoverMutex?.close();
}
});
super.initState();
_focusNode = FocusNode()
..addListener(() {
if (_focusNode.hasFocus) {
widget.popoverMutex?.close();
}
});
widget.popoverMutex?.listenOnPopoverChanged(() {
if (_focusNode.hasFocus) {
_focusNode.unfocus();
}
});
super.initState();
}
@override

View File

@ -13,8 +13,8 @@ import 'package:flowy_infra_ui/widget/spacing.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../layout/sizes.dart';
import '../../common/type_option_separator.dart';
import '../../../../layout/sizes.dart';
import '../../../common/type_option_separator.dart';
class SelectOptionTypeOptionEditor extends StatelessWidget {
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/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 '../field_type_option_editor.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'builder.dart';
import 'select_option.dart';
import 'select/select_option.dart';
class SingleSelectTypeOptionWidgetBuilder extends TypeOptionWidgetBuilder {
final SingleSelectTypeOptionWidget _widget;
SingleSelectTypeOptionWidgetBuilder(
SingleSelectTypeOptionContext singleSelectTypeOption,
PopoverMutex popoverMutex,
) : _widget = SingleSelectTypeOptionWidget(
selectOptionAction: SingleSelectAction(
fieldId: singleSelectTypeOption.fieldId,
viewId: singleSelectTypeOption.viewId,
typeOptionContext: singleSelectTypeOption,
),
popoverMutex: popoverMutex,
);
class SingleSelectTypeOptionEditorFactory implements TypeOptionEditorFactory {
const SingleSelectTypeOptionEditorFactory();
@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(
options: selectOptionAction.typeOption.options,
beginEdit: () {
PopoverContainer.of(context).closeAll();
},
options: typeOption.options,
beginEdit: () => PopoverContainer.of(context).closeAll(),
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/timestamp_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/plugins/database_view/grid/presentation/layout/sizes.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/common/type_option_separator.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/field_type_option_editor.dart';
import 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/include_time_button.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/date_entities.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:appflowy_popover/appflowy_popover.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 'date.dart';
import 'date/date_time_format.dart';
class TimestampTypeOptionWidgetBuilder extends TypeOptionWidgetBuilder {
TimestampTypeOptionWidgetBuilder(
TimestampTypeOptionContext typeOptionContext,
PopoverMutex popoverMutex,
) : _widget = TimestampTypeOptionWidget(
typeOptionContext: typeOptionContext,
popoverMutex: popoverMutex,
);
final TimestampTypeOptionWidget _widget;
class TimestampTypeOptionEditorFactory implements TypeOptionEditorFactory {
const TimestampTypeOptionEditorFactory();
@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 {
const TimestampTypeOptionWidget({
super.key,
required this.typeOptionContext,
required this.popoverMutex,
});
final TimestampTypeOptionContext typeOptionContext;
final PopoverMutex popoverMutex;
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) =>
TimestampTypeOptionBloc(typeOptionContext: typeOptionContext),
child: BlocConsumer<TimestampTypeOptionBloc, TimestampTypeOptionState>(
listener: (context, state) =>
typeOptionContext.typeOption = state.typeOption,
builder: (context, state) {
final List<Widget> children = [
const TypeOptionSeparator(),
_renderDateFormatButton(context, state.typeOption.dateFormat),
_renderTimeFormatButton(context, state.typeOption.timeFormat),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 12.0),
child: IncludeTimeButton(
onChanged: (value) => context
.read<TimestampTypeOptionBloc>()
.add(TimestampTypeOptionEvent.includeTime(!value)),
value: state.typeOption.includeTime,
),
),
];
return ListView.separated(
shrinkWrap: true,
separatorBuilder: (context, index) {
if (index == 0) {
return const SizedBox.shrink();
} else {
return VSpace(GridSize.typeOptionSeparatorHeight);
}
return SeparatedColumn(
mainAxisSize: MainAxisSize.min,
separatorBuilder: () => VSpace(GridSize.typeOptionSeparatorHeight),
children: [
_renderDateFormatButton(typeOption, popoverMutex, onTypeOptionUpdated),
_renderTimeFormatButton(typeOption, popoverMutex, onTypeOptionUpdated),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 12.0),
child: IncludeTimeButton(
onChanged: (value) {
final newTypeOption = _updateTypeOption(
typeOption: typeOption,
includeTime: !value,
);
onTypeOptionUpdated(newTypeOption.writeToBuffer());
},
itemCount: children.length,
itemBuilder: (BuildContext context, int index) => children[index],
);
},
),
value: typeOption.includeTime,
),
),
],
);
}
Widget _renderDateFormatButton(
BuildContext context,
DateFormatPB dataFormat,
TimestampTypeOptionPB typeOption,
PopoverMutex popoverMutex,
TypeOptionDataCallback onTypeOptionUpdated,
) {
return AppFlowyPopover(
mutex: popoverMutex,
@ -90,15 +56,17 @@ class TimestampTypeOptionWidget extends TypeOptionWidget {
triggerActions: PopoverTriggerFlags.hover | PopoverTriggerFlags.click,
offset: const Offset(8, 0),
constraints: BoxConstraints.loose(const Size(460, 440)),
popupBuilder: (popoverContext) => DateFormatList(
selectedFormat: dataFormat,
onSelected: (format) {
context
.read<TimestampTypeOptionBloc>()
.add(TimestampTypeOptionEvent.didSelectDateFormat(format));
PopoverContainer.of(popoverContext).close();
},
),
popupBuilder: (popoverContext) {
return DateFormatList(
selectedFormat: typeOption.dateFormat,
onSelected: (format) {
final newTypeOption =
_updateTypeOption(typeOption: typeOption, dateFormat: format);
onTypeOptionUpdated(newTypeOption.writeToBuffer());
PopoverContainer.of(popoverContext).close();
},
);
},
child: const Padding(
padding: EdgeInsets.symmetric(horizontal: 12.0),
child: DateFormatButton(),
@ -107,8 +75,9 @@ class TimestampTypeOptionWidget extends TypeOptionWidget {
}
Widget _renderTimeFormatButton(
BuildContext context,
TimeFormatPB timeFormat,
TimestampTypeOptionPB typeOption,
PopoverMutex popoverMutex,
TypeOptionDataCallback onTypeOptionUpdated,
) {
return AppFlowyPopover(
mutex: popoverMutex,
@ -116,19 +85,47 @@ class TimestampTypeOptionWidget extends TypeOptionWidget {
triggerActions: PopoverTriggerFlags.hover | PopoverTriggerFlags.click,
offset: const Offset(8, 0),
constraints: BoxConstraints.loose(const Size(460, 440)),
popupBuilder: (BuildContext popoverContext) => TimeFormatList(
selectedFormat: timeFormat,
onSelected: (format) {
context
.read<TimestampTypeOptionBloc>()
.add(TimestampTypeOptionEvent.didSelectTimeFormat(format));
PopoverContainer.of(popoverContext).close();
},
),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 12.0),
child: TimeFormatButton(timeFormat: timeFormat),
popupBuilder: (BuildContext popoverContext) {
return TimeFormatList(
selectedFormat: typeOption.timeFormat,
onSelected: (format) {
final newTypeOption =
_updateTypeOption(typeOption: typeOption, timeFormat: format);
onTypeOptionUpdated(newTypeOption.writeToBuffer());
PopoverContainer.of(popoverContext).close();
},
);
},
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 'builder.dart';
class URLTypeOptionWidgetBuilder extends TypeOptionWidgetBuilder {
URLTypeOptionWidgetBuilder(URLTypeOptionContext typeOptionContext);
class URLTypeOptionEditorFactory implements TypeOptionEditorFactory {
const URLTypeOptionEditorFactory();
@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> {
late PopoverController _popover;
final PopoverController _popover = PopoverController();
late final DateCellController _cellController;
late DateCellBloc _cellBloc;
@override
void initState() {
_popover = PopoverController();
final cellController =
widget.cellControllerBuilder.build() as DateCellController;
_cellBloc = DateCellBloc(cellController: cellController)
..add(const DateCellEvent.initial());
super.initState();
_cellController =
widget.cellControllerBuilder.build() as DateCellController;
_cellBloc = DateCellBloc(cellController: _cellController)
..add(const DateCellEvent.initial());
}
@override
@ -93,8 +93,7 @@ class _DateCellState extends GridCellState<GridDateCell> {
),
popupBuilder: (BuildContext popoverContent) {
return DateCellEditor(
cellController: widget.cellControllerBuilder.build()
as DateCellController,
cellController: _cellController,
onDismissed: () =>
widget.cellContainerNotifier.isFocus = false,
);
@ -111,8 +110,7 @@ class _DateCellState extends GridCellState<GridDateCell> {
padding: EdgeInsets.zero,
builder: (context) {
return MobileDateCellEditScreen(
controller: widget.cellControllerBuilder.build()
as DateCellController,
controller: _cellController,
showAsFullScreen: false,
);
},
@ -169,8 +167,7 @@ class _DateCellState extends GridCellState<GridDateCell> {
Theme.of(context).colorScheme.secondaryContainer,
builder: (context) {
return MobileDateCellEditScreen(
controller: widget.cellControllerBuilder.build()
as DateCellController,
controller: _cellController,
showAsFullScreen: false,
);
},
@ -186,6 +183,7 @@ class _DateCellState extends GridCellState<GridDateCell> {
@override
Future<void> dispose() async {
_cellBloc.close();
_cellController.dispose();
super.dispose();
}

View File

@ -37,7 +37,6 @@ class DateCellBloc extends Bloc<DateCellEvent, DateCellState> {
cellController.removeListener(_onCellChangedFn!);
_onCellChangedFn = null;
}
await cellController.dispose();
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/date_cell_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/protobuf/flowy-database2/date_entities.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:protobuf/protobuf.dart';
part 'date_cal_bloc.freezed.dart';
part 'date_cell_editor_bloc.freezed.dart';
class DateCellCalendarBloc
extends Bloc<DateCellCalendarEvent, DateCellCalendarState> {
class DateCellEditorBloc
extends Bloc<DateCellEditorEvent, DateCellEditorState> {
final DateCellBackendService _dateCellBackendService;
final DateCellController cellController;
void Function()? _onCellChangedFn;
DateCellCalendarBloc({
required DateTypeOptionPB dateTypeOptionPB,
required DateCellDataPB? cellData,
DateCellEditorBloc({
required this.cellController,
}) : _dateCellBackendService = DateCellBackendService(
viewId: cellController.viewId,
fieldId: cellController.fieldId,
rowId: cellController.rowId,
),
super(DateCellCalendarState.initial(dateTypeOptionPB, cellData)) {
on<DateCellCalendarEvent>(
super(DateCellEditorState.initial(cellController)) {
on<DateCellEditorEvent>(
(event, emit) async {
await event.when(
initial: () async => _startListening(),
@ -222,7 +221,7 @@ class DateCellCalendarBloc
if (!isClosed &&
(state.parseEndTimeError != null || state.parseTimeError != null)) {
add(
const DateCellCalendarEvent.didReceiveTimeFormatError(null, null),
const DateCellEditorEvent.didReceiveTimeFormatError(null, null),
);
}
},
@ -237,7 +236,7 @@ class DateCellCalendarBloc
? (timeFormatPrompt(err), null)
: (null, timeFormatPrompt(err));
add(
DateCellCalendarEvent.didReceiveTimeFormatError(
DateCellEditorEvent.didReceiveTimeFormatError(
startError,
endError,
),
@ -259,7 +258,7 @@ class DateCellCalendarBloc
}
add(
const DateCellCalendarEvent.didReceiveTimeFormatError(null, null),
const DateCellEditorEvent.didReceiveTimeFormatError(null, null),
);
},
(err) => Log.error(err),
@ -300,7 +299,6 @@ class DateCellCalendarBloc
cellController.removeListener(_onCellChangedFn!);
_onCellChangedFn = null;
}
await cellController.dispose();
return super.close();
}
@ -308,14 +306,14 @@ class DateCellCalendarBloc
_onCellChangedFn = cellController.startListening(
onCellChanged: ((cell) {
if (!isClosed) {
add(DateCellCalendarEvent.didReceiveCellUpdate(cell));
add(DateCellEditorEvent.didReceiveCellUpdate(cell));
}
}),
);
}
Future<void>? _updateTypeOption(
Emitter<DateCellCalendarState> emit, {
Emitter<DateCellEditorState> emit, {
DateFormatPB? dateFormat,
TimeFormatPB? timeFormat,
}) async {
@ -349,49 +347,49 @@ class DateCellCalendarBloc
}
@freezed
class DateCellCalendarEvent with _$DateCellCalendarEvent {
class DateCellEditorEvent with _$DateCellEditorEvent {
// initial event
const factory DateCellCalendarEvent.initial() = _Initial;
const factory DateCellEditorEvent.initial() = _Initial;
// notification that cell is updated in the backend
const factory DateCellCalendarEvent.didReceiveCellUpdate(
const factory DateCellEditorEvent.didReceiveCellUpdate(
DateCellDataPB? data,
) = _DidReceiveCellUpdate;
const factory DateCellCalendarEvent.didReceiveTimeFormatError(
const factory DateCellEditorEvent.didReceiveTimeFormatError(
String? parseTimeError,
String? parseEndTimeError,
) = _DidReceiveTimeFormatError;
// date cell data is modified
const factory DateCellCalendarEvent.selectDay(DateTime day) = _SelectDay;
const factory DateCellCalendarEvent.selectDateRange(
const factory DateCellEditorEvent.selectDay(DateTime day) = _SelectDay;
const factory DateCellEditorEvent.selectDateRange(
DateTime? start,
DateTime? end,
) = _SelectDateRange;
const factory DateCellCalendarEvent.setStartDay(
const factory DateCellEditorEvent.setStartDay(
DateTime startDay,
) = _SetStartDay;
const factory DateCellCalendarEvent.setEndDay(
const factory DateCellEditorEvent.setEndDay(
DateTime endDay,
) = _SetEndDay;
const factory DateCellCalendarEvent.setTime(String time) = _Time;
const factory DateCellCalendarEvent.setEndTime(String endTime) = _EndTime;
const factory DateCellCalendarEvent.setIncludeTime(bool includeTime) =
const factory DateCellEditorEvent.setTime(String time) = _Time;
const factory DateCellEditorEvent.setEndTime(String endTime) = _EndTime;
const factory DateCellEditorEvent.setIncludeTime(bool includeTime) =
_IncludeTime;
const factory DateCellCalendarEvent.setIsRange(bool isRange) = _IsRange;
const factory DateCellEditorEvent.setIsRange(bool isRange) = _IsRange;
// date field type options are modified
const factory DateCellCalendarEvent.setTimeFormat(TimeFormatPB timeFormat) =
const factory DateCellEditorEvent.setTimeFormat(TimeFormatPB timeFormat) =
_TimeFormat;
const factory DateCellCalendarEvent.setDateFormat(DateFormatPB dateFormat) =
const factory DateCellEditorEvent.setDateFormat(DateFormatPB dateFormat) =
_DateFormat;
const factory DateCellCalendarEvent.clearDate() = _ClearDate;
const factory DateCellEditorEvent.clearDate() = _ClearDate;
}
@freezed
class DateCellCalendarState with _$DateCellCalendarState {
const factory DateCellCalendarState({
class DateCellEditorState with _$DateCellEditorState {
const factory DateCellEditorState({
// the date field's type option
required DateTypeOptionPB dateTypeOptionPB,
@ -413,15 +411,14 @@ class DateCellCalendarState with _$DateCellCalendarState {
required String? parseTimeError,
required String? parseEndTimeError,
required String timeHintText,
}) = _DateCellCalendarState;
}) = _DateCellEditorState;
factory DateCellCalendarState.initial(
DateTypeOptionPB dateTypeOptionPB,
DateCellDataPB? cellData,
) {
factory DateCellEditorState.initial(DateCellController controller) {
final typeOption = controller.getTypeOption(DateTypeOptionDataParser());
final cellData = controller.getCellData();
final dateCellData = _dateDataFromCellData(cellData);
return DateCellCalendarState(
dateTypeOptionPB: dateTypeOptionPB,
return DateCellEditorState(
dateTypeOptionPB: typeOption,
startDay: dateCellData.isRange ? dateCellData.dateTime : null,
endDay: dateCellData.isRange ? dateCellData.endDateTime : null,
dateTime: dateCellData.dateTime,
@ -434,7 +431,7 @@ class DateCellCalendarState with _$DateCellCalendarState {
isRange: dateCellData.isRange,
parseTimeError: null,
parseEndTimeError: null,
timeHintText: _timeHintText(dateTypeOptionPB),
timeHintText: _timeHintText(typeOption),
);
}
}
@ -450,13 +447,13 @@ String _timeHintText(DateTypeOptionPB typeOption) {
}
}
DateCellData _dateDataFromCellData(
_DateCellData _dateDataFromCellData(
DateCellDataPB? cellData,
) {
// a null DateCellDataPB may be returned, indicating that all the fields are
// their default values: empty strings and false booleans
if (cellData == null) {
return DateCellData(
return _DateCellData(
dateTime: null,
endDateTime: null,
timeStr: null,
@ -492,7 +489,7 @@ DateCellData _dateDataFromCellData(
}
final String dateStr = cellData.date;
return DateCellData(
return _DateCellData(
dateTime: dateTime,
endDateTime: endDateTime,
timeStr: timeStr,
@ -504,7 +501,7 @@ DateCellData _dateDataFromCellData(
);
}
class DateCellData {
class _DateCellData {
final DateTime? dateTime;
final DateTime? endDateTime;
final String? timeStr;
@ -514,7 +511,7 @@ class DateCellData {
final String? dateStr;
final String? endDateStr;
DateCellData({
_DateCellData({
required this.dateTime,
required this.endDateTime,
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/field/type_option/type_option_context.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:dartz/dartz.dart' show Either;
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'date_cal_bloc.dart';
import 'date_cell_editor_bloc.dart';
class DateCellEditor extends StatefulWidget {
const DateCellEditor({
@ -27,48 +21,6 @@ class DateCellEditor extends StatefulWidget {
}
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();
@override
@ -80,22 +32,19 @@ class _CellCalendarWidgetState extends State<_CellCalendarWidget> {
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => DateCellCalendarBloc(
dateTypeOptionPB: widget.dateTypeOptionPB,
cellData: widget.cellContext.getCellData(),
cellController: widget.cellContext,
)..add(const DateCellCalendarEvent.initial()),
child: BlocBuilder<DateCellCalendarBloc, DateCellCalendarState>(
create: (context) => DateCellEditorBloc(
cellController: widget.cellController,
)..add(const DateCellEditorEvent.initial()),
child: BlocBuilder<DateCellEditorBloc, DateCellEditorState>(
builder: (context, state) {
final bloc = context.read<DateCellEditorBloc>();
return AppFlowyDatePicker(
includeTime: state.includeTime,
onIncludeTimeChanged: (value) => context
.read<DateCellCalendarBloc>()
.add(DateCellCalendarEvent.setIncludeTime(!value)),
onIncludeTimeChanged: (value) =>
bloc.add(DateCellEditorEvent.setIncludeTime(!value)),
isRange: state.isRange,
onIsRangeChanged: (value) => context
.read<DateCellCalendarBloc>()
.add(DateCellCalendarEvent.setIsRange(!value)),
onIsRangeChanged: (value) =>
bloc.add(DateCellEditorEvent.setIsRange(!value)),
dateFormat: state.dateTypeOptionPB.dateFormat,
timeFormat: state.dateTypeOptionPB.timeFormat,
selectedDay: state.dateTime,
@ -105,28 +54,28 @@ class _CellCalendarWidgetState extends State<_CellCalendarWidget> {
parseEndTimeError: state.parseEndTimeError,
parseTimeError: state.parseTimeError,
popoverMutex: popoverMutex,
onStartTimeSubmitted: (timeStr) => context
.read<DateCellCalendarBloc>()
.add(DateCellCalendarEvent.setTime(timeStr)),
onEndTimeSubmitted: (timeStr) => context
.read<DateCellCalendarBloc>()
.add(DateCellCalendarEvent.setEndTime(timeStr)),
onDaySelected: (selectedDay, _) => context
.read<DateCellCalendarBloc>()
.add(DateCellCalendarEvent.selectDay(selectedDay)),
onRangeSelected: (start, end, _) => context
.read<DateCellCalendarBloc>()
.add(DateCellCalendarEvent.selectDateRange(start, end)),
onStartTimeSubmitted: (timeStr) {
bloc.add(DateCellEditorEvent.setTime(timeStr));
},
onEndTimeSubmitted: (timeStr) {
bloc.add(DateCellEditorEvent.setEndTime(timeStr));
},
onDaySelected: (selectedDay, _) {
bloc.add(DateCellEditorEvent.selectDay(selectedDay));
},
onRangeSelected: (start, end, _) {
bloc.add(DateCellEditorEvent.selectDateRange(start, end));
},
allowFormatChanges: true,
onDateFormatChanged: (format) => context
.read<DateCellCalendarBloc>()
.add(DateCellCalendarEvent.setDateFormat(format)),
onTimeFormatChanged: (format) => context
.read<DateCellCalendarBloc>()
.add(DateCellCalendarEvent.setTimeFormat(format)),
onClearDate: () => context
.read<DateCellCalendarBloc>()
.add(const DateCellCalendarEvent.clearDate()),
onDateFormatChanged: (format) {
bloc.add(DateCellEditorEvent.setDateFormat(format));
},
onTimeFormatChanged: (format) {
bloc.add(DateCellEditorEvent.setTimeFormat(format));
},
onClearDate: () {
bloc.add(const DateCellEditorEvent.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:table_calendar/table_calendar.dart';
import 'date_cal_bloc.dart';
import 'date_cell_editor_bloc.dart';
class MobileDatePicker extends StatefulWidget {
const MobileDatePicker({
@ -49,7 +49,7 @@ class _MobileDatePickerState extends State<MobileDatePicker> {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: BlocBuilder<DateCellCalendarBloc, DateCellCalendarState>(
child: BlocBuilder<DateCellEditorBloc, DateCellEditorState>(
builder: (context, state) {
return TableCalendar(
firstDay: kFirstDay,
@ -124,13 +124,13 @@ class _MobileDatePickerState extends State<MobileDatePicker> {
selectedDayPredicate: (day) =>
state.isRange ? false : isSameDay(state.dateTime, day),
onDaySelected: (selectedDay, focusedDay) {
context.read<DateCellCalendarBloc>().add(
DateCellCalendarEvent.selectDay(selectedDay),
context.read<DateCellEditorBloc>().add(
DateCellEditorEvent.selectDay(selectedDay),
);
},
onRangeSelected: (start, end, focusedDay) {
context.read<DateCellCalendarBloc>().add(
DateCellCalendarEvent.selectDateRange(start, end),
context.read<DateCellEditorBloc>().add(
DateCellEditorEvent.selectDateRange(start, end),
);
},
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/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 'select_option_editor_bloc.dart';
import 'text_field.dart';

View File

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

View File

@ -1,10 +1,10 @@
import 'package:appflowy/workspace/presentation/widgets/date_picker/utils/layout.dart';
import 'package:flutter/material.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_backend/protobuf/flowy-database2/date_entities.pbenum.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/widgets.dart';
import '../utils/layout.dart';
class DateTimeSetting extends StatefulWidget {
const DateTimeSetting({
@ -52,9 +52,9 @@ class _DateTimeSettingState extends State<DateTimeSetting> {
selectedFormat: widget.timeFormat,
onSelected: _onTimeFormatChanged,
),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 6.0),
child: TimeFormatButton(timeFormat: widget.timeFormat),
child: const Padding(
padding: EdgeInsets.symmetric(horizontal: 6.0),
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/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_backend/protobuf/flowy-database2/field_entities.pb.dart';
import 'package:flutter_test/flutter_test.dart';
@ -34,14 +33,9 @@ void main() {
await boardResponseFuture();
final fieldInfo = context.singleSelectFieldContext();
final loader = FieldTypeOptionLoader(
viewId: context.gridView.id,
field: fieldInfo.field,
);
final editorBloc = FieldEditorBloc(
viewId: context.gridView.id,
loader: loader,
field: fieldInfo.field,
fieldController: context.fieldController,
)..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_editor_bloc.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_controller.dart';
import 'package:appflowy/plugins/database_view/board/board.dart';
@ -82,15 +81,9 @@ class BoardTestContext {
FieldEditorBloc makeFieldEditor({
required FieldInfo fieldInfo,
}) {
final loader = FieldTypeOptionLoader(
viewId: gridView.id,
field: fieldInfo.field,
);
final editorBloc = FieldEditorBloc(
viewId: databaseController.viewId,
fieldController: fieldController,
loader: loader,
field: fieldInfo.field,
);
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/type_option/type_option_context.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart';
import 'package:flutter_test/flutter_test.dart';
import '../util.dart';
Future<FieldEditorBloc> createEditorBloc(AppFlowyGridTest gridTest) async {
final context = await gridTest.createTestGrid();
final fieldInfo = context.singleSelectFieldContext();
final loader = FieldTypeOptionLoader(
viewId: context.gridView.id,
field: fieldInfo.field,
);
return FieldEditorBloc(
viewId: context.gridView.id,
fieldController: context.fieldController,
loader: loader,
field: fieldInfo.field,
)..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_info.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_controller.dart';
import 'package:appflowy/plugins/database_view/application/database_controller.dart';
@ -134,16 +133,11 @@ Future<FieldEditorBloc> createFieldEditor({
);
await gridResponseFuture();
return result.fold(
(data) {
final loader = FieldTypeOptionLoader(
viewId: databaseController.viewId,
field: data.field_2,
);
(field) {
return FieldEditorBloc(
viewId: databaseController.viewId,
fieldController: databaseController.fieldController,
loader: loader,
field: data.field_2,
field: field,
);
},
(err) => throw Exception(err),

View File

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

View File

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

View File

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

View File

@ -25,7 +25,7 @@ import {
DatabaseEventGetAllFieldSettings,
} from '@/services/backend/events/flowy-database2';
import { Field, pbToField } from './field_types';
import { getTypeOption } from './type_option';
import { bytesToTypeOption } from './type_option';
import { Database } from '$app/components/database/application';
export async function getFields(
@ -64,7 +64,7 @@ export async function getFields(
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) {
typeOptions[item.id] = typeOption;
@ -110,7 +110,7 @@ export async function createField({
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> {

View File

@ -1,27 +1,8 @@
import { FieldType, TypeOptionPathPB, TypeOptionChangesetPB } from '@/services/backend';
import { FieldType, TypeOptionChangesetPB } from '@/services/backend';
import {
DatabaseEventGetTypeOption,
DatabaseEventUpdateFieldTypeOption,
} from '@/services/backend/events/flowy-database2';
import { bytesToTypeOption, 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);
}
import { UndeterminedTypeOptionData, typeOptionDataToPB } from './type_option_types';
export async function updateTypeOption(
viewId: string,

View File

@ -11,7 +11,6 @@ import {
URLCellController,
} from '$app/stores/effects/database/cell/controller_builder';
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 { FieldInfo } from '$app/stores/effects/database/field/field_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) {
const svc = new TypeOptionBackendService(viewId);
const typeOptionPB = await svc.getTypeOption(fieldId, fieldType).then((result) => result.unwrap());
export async function assertFieldName(controller: TypeOptionController, expected: string) {
const fieldInfo = controller.getFieldInfo();
if (typeOptionPB.field.name !== expected) {
throw Error('Expect field name:' + expected + 'but receive:' + typeOptionPB.field.name);
if (fieldInfo.field.name !== expected) {
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');
const typeOptionController = new TypeOptionController(viewId, Some(fieldInfo));
const singleSelectTypeOptionContext = makeSingleSelectTypeOptionContext(typeOptionController);
const singleSelectTypeOptionPB: SingleSelectTypeOptionPB = await singleSelectTypeOptionContext
.getTypeOption()
.then((result) => result.unwrap());
const singleSelectTypeOptionPB: SingleSelectTypeOptionPB = singleSelectTypeOptionContext
.getTypeOption();
const backendSvc = new SelectOptionBackendService(viewId, fieldInfo.field.id);

View File

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

View File

@ -2,11 +2,11 @@ import { CellIdentifier } from './cell_bd_svc';
import { CellCache, CellCacheKey } from './cell_cache';
import { CellDataLoader } from './data_parser';
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 { CellObserver } from './cell_observer';
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';
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) => {
const result = await this.cellDataPersistence.save(data);

View File

@ -2,14 +2,11 @@ import {
DeleteFieldPayloadPB,
DuplicateFieldPayloadPB,
FieldChangesetPB,
FieldType,
TypeOptionChangesetPB,
TypeOptionPathPB,
} from '@/services/backend';
import {
DatabaseEventDeleteField,
DatabaseEventDuplicateField,
DatabaseEventGetTypeOption,
DatabaseEventUpdateField,
DatabaseEventUpdateFieldTypeOption,
} from '@/services/backend/events/flowy-database2';
@ -64,14 +61,4 @@ export class FieldBackendService {
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 {
DatabaseEventCreateField,
DatabaseEventGetTypeOption,
DatabaseEventUpdateFieldType,
} from '@/services/backend/events/flowy-database2';
@ -14,16 +13,6 @@ export class TypeOptionBackendService {
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) => {
const payload = UpdateFieldTypePayloadPB.fromObject({
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 {
CheckboxTypeOptionPB,
ChecklistTypeOptionPB,
DateTypeOptionPB,
FlowyError,
MultiSelectTypeOptionPB,
NumberTypeOptionPB,
SingleSelectTypeOptionPB,
@ -190,17 +189,12 @@ export class TypeOptionContext<T> {
return this.controller.viewId;
}
getTypeOption = async (): Promise<Result<T, FlowyError>> => {
const result = await this.controller.getTypeOption();
getTypeOption = (): T => {
const type_option_data = this.controller.getTypeOption();
const typeOption = this.parser.deserialize(type_option_data);
if (result.ok) {
const typeOption = this.parser.deserialize(result.val.type_option_data);
this.typeOption = Some(typeOption);
return Ok(typeOption);
} else {
return result;
}
this.typeOption = Some(typeOption);
return typeOption;
};
// 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 { FieldBackendService } from '../field_bd_svc';
import { Log } from '$app/utils/log';
@ -8,7 +8,7 @@ import { TypeOptionBackendService } from './type_option_bd_svc';
export class TypeOptionController {
private fieldNotifier = new ChangeNotifier<FieldPB>();
private typeOptionData: Option<TypeOptionPB>;
private field: Option<FieldPB>;
private fieldBackendSvc?: FieldBackendService;
private typeOptionBackendSvc: TypeOptionBackendService;
@ -18,7 +18,13 @@ export class TypeOptionController {
private readonly initialFieldInfo: Option<FieldInfo> = None,
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);
}
@ -27,8 +33,6 @@ export class TypeOptionController {
initialize = async () => {
if (this.initialFieldInfo.none) {
await this.createTypeOption(this.defaultFieldType);
} else {
await this.getTypeOption();
}
};
@ -41,7 +45,7 @@ export class TypeOptionController {
}
getFieldInfo = (): FieldInfo => {
if (this.typeOptionData.none) {
if (this.field.none) {
if (this.initialFieldInfo.some) {
return this.initialFieldInfo.val;
} 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) => {
const result = await this.typeOptionBackendSvc.updateTypeOptionType(this.fieldId, fieldType);
if (result.ok) {
const getResult = await this.typeOptionBackendSvc.getTypeOption(this.fieldId, fieldType);
if (getResult.ok) {
this.updateTypeOptionData(getResult.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);
if (this.field.some) {
this.field.val.field_type = fieldType;
await this.typeOptionBackendSvc.updateTypeOptionType(this.fieldId, fieldType).then((result) => {
if (result.err) {
Log.error(result.val);
}
});
this.fieldNotifier.notify(this.field.val);
} else {
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 () => {
if (this.fieldBackendSvc) {
void this.fieldBackendSvc.updateField({ visibility: false });
@ -103,8 +103,8 @@ export class TypeOptionController {
};
saveTypeOption = async (data: Uint8Array) => {
if (this.typeOptionData.some) {
this.typeOptionData.val.type_option_data = data;
if (this.field.some) {
this.field.val.type_option_data = data;
await this.fieldBackendSvc?.updateTypeOption(data).then((result) => {
if (result.err) {
Log.error(result.val);
@ -132,29 +132,27 @@ export class TypeOptionController {
};
// Returns the type option for specific field with specific fieldType
getTypeOption = async () => {
return this.typeOptionBackendSvc.getTypeOption(this.fieldId, this.fieldType).then((result) => {
if (result.ok) {
this.updateTypeOptionData(result.val);
}
return result;
});
getTypeOption = () => {
if (this.field.some) {
return this.field.val.type_option_data;
} else {
throw Error('Unexpected empty type option data. Should call initialize first');
}
};
private createTypeOption = (fieldType: FieldType) => {
return this.typeOptionBackendSvc.createTypeOption(fieldType).then((result) => {
if (result.ok) {
this.updateTypeOptionData(result.val);
}
private createTypeOption = async (fieldType: FieldType) => {
const result = await this.typeOptionBackendSvc.createTypeOption(fieldType);
return result;
});
if (result.ok) {
this.updateField(result.val);
}
return result;
};
private updateTypeOptionData = (typeOptionData: TypeOptionPB) => {
this.typeOptionData = Some(typeOptionData);
this.fieldBackendSvc = new FieldBackendService(this.viewId, typeOptionData.field.id);
this.fieldNotifier.notify(typeOptionData.field);
private updateField = (field: FieldPB) => {
this.field = Some(field);
this.fieldBackendSvc = new FieldBackendService(this.viewId, field.id);
this.fieldNotifier.notify(field);
};
}

View File

@ -120,8 +120,7 @@ impl EventIntegrationTest {
})
.async_send()
.await
.parse::<TypeOptionPB>()
.field
.parse::<FieldPB>()
}
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::position_entities::OrderObjectPositionPB;
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.
#[derive(Debug, Clone, Default, ProtoBuf)]
@ -35,17 +36,25 @@ pub struct FieldPB {
#[pb(index = 6)]
pub is_primary: bool,
#[pb(index = 7)]
pub type_option_data: Vec<u8>,
}
impl std::convert::From<Field> for FieldPB {
fn from(field: Field) -> Self {
impl FieldPB {
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 {
id: field.id,
name: field.name,
field_type: FieldType::from(field.field_type),
field_type,
visibility: field.visibility,
width: field.width as i32,
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,
}
impl IndexFieldPB {
pub fn from_field(field: Field, index: usize) -> Self {
Self {
field: FieldPB::from(field),
index: index as i32,
}
}
}
#[derive(Debug, Default, ProtoBuf)]
pub struct CreateFieldPayloadPB {
#[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]
#[derive(Debug, Default, ProtoBuf)]
pub struct RepeatedFieldPB {

View File

@ -13,7 +13,7 @@ use crate::manager::DatabaseManager;
use crate::services::cell::CellBuilder;
use crate::services::field::checklist_type_option::ChecklistCellChangeset;
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::group::GroupChangeset;
@ -160,7 +160,7 @@ pub(crate) async fn get_fields_handler(
let fields = database_editor
.get_fields(&params.view_id, params.field_ids)
.into_iter()
.map(FieldPB::from)
.map(FieldPB::new)
.collect::<Vec<FieldPB>>()
.into();
data_result_ok(fields)
@ -178,7 +178,7 @@ pub(crate) async fn get_primary_field_handler(
.get_fields(&view_id, None)
.into_iter()
.filter(|field| field.is_primary)
.map(FieldPB::from)
.map(FieldPB::new)
.collect::<Vec<FieldPB>>();
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?;
if let Some(old_field) = database_editor.get_field(&params.field_id) {
let field_type = FieldType::from(old_field.field_type);
let type_option_data =
type_option_data_from_pb_or_default(params.type_option_data, &field_type);
let type_option_data = type_option_data_from_pb(params.type_option_data, &field_type)?;
database_editor
.update_field_type_option(
&params.view_id,
@ -293,46 +292,19 @@ pub(crate) async fn duplicate_field_handler(
Ok(())
}
/// Return the FieldTypeOptionData if the Field exists otherwise return record not found error.
#[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.
/// 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 create_field_handler(
data: AFPluginData<CreateFieldPayloadPB>,
manager: AFPluginState<Weak<DatabaseManager>>,
) -> DataResult<TypeOptionPB, FlowyError> {
) -> DataResult<FieldPB, FlowyError> {
let manager = upgrade_manager(manager)?;
let params: CreateFieldParams = data.into_inner().try_into()?;
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)
}

View File

@ -30,7 +30,6 @@ pub fn init(database_manager: Weak<DatabaseManager>) -> AFPlugin {
.event(DatabaseEvent::UpdateFieldType, switch_to_field_handler)
.event(DatabaseEvent::DuplicateField, duplicate_field_handler)
.event(DatabaseEvent::MoveField, move_field_handler)
.event(DatabaseEvent::GetTypeOption, get_field_type_option_data_handler)
.event(DatabaseEvent::CreateField, create_field_handler)
// Row
.event(DatabaseEvent::CreateRow, create_row_handler)
@ -174,18 +173,9 @@ pub enum DatabaseEvent {
#[event(input = "MoveFieldPayloadPB")]
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
/// TypeOptionData.
#[event(input = "CreateFieldPayloadPB", output = "TypeOptionPB")]
#[event(input = "CreateFieldPayloadPB", output = "FieldPB")]
CreateField = 24,
#[event(input = "DatabaseViewIdPB", output = "FieldPB")]

View File

@ -1,7 +1,6 @@
use std::collections::HashMap;
use std::sync::Arc;
use bytes::Bytes;
use collab_database::database::MutexDatabase;
use collab_database::fields::{Field, TypeOptionData};
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::{
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,
SelectOptionIds, TimestampCellData, TypeOptionCellDataHandler, TypeOptionCellExt,
type_option_data_from_pb, SelectOptionCellChangeset, SelectOptionIds, TimestampCellData,
TypeOptionCellDataHandler, TypeOptionCellExt,
};
use crate::services::field_settings::{
default_field_settings_by_layout_map, FieldSettings, FieldSettingsChangesetParams,
@ -469,28 +468,20 @@ impl DatabaseEditor {
Ok(None)
}
pub async fn get_field_type_option_data(&self, field_id: &str) -> Option<(Field, Bytes)> {
let field = self.database.lock().fields.get_field(field_id);
field.map(|field| {
let field_type = FieldType::from(field.field_type);
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) {
pub async fn create_field_with_type_option(
&self,
params: CreateFieldParams,
) -> FlowyResult<FieldPB> {
let name = params
.field_name
.clone()
.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),
Some(type_option_data) => {
type_option_data_from_pb_or_default(type_option_data.clone(), &params.field_type)
},
};
let type_option_data = params
.type_option_data
.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(
&params.view_id,
name,
@ -499,7 +490,7 @@ impl DatabaseEditor {
|field| {
field
.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(),
);
@ -508,10 +499,7 @@ impl DatabaseEditor {
.notify_did_insert_database_field(field.clone(), index)
.await;
(
field,
type_option_to_pb(type_option_data, &params.field_type),
)
Ok(FieldPB::new(field))
}
pub async fn move_field(&self, params: MoveFieldParams) -> FlowyResult<()> {
@ -539,7 +527,10 @@ impl DatabaseEditor {
if let Some(index) = new_index {
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 {
view_id: params.view_id,
inserted_fields: vec![insert_field],
@ -1058,7 +1049,10 @@ impl DatabaseEditor {
#[tracing::instrument(level = "trace", skip_all, err)]
async fn notify_did_insert_database_field(&self, field: Field, index: usize) -> FlowyResult<()> {
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 _ = self.notify_did_update_database(notified_changeset).await;
Ok(())
@ -1629,7 +1623,7 @@ fn notify_did_update_database_field(
};
if let Some(field) = field {
let updated_field = FieldPB::from(field);
let updated_field = FieldPB::new(field);
let notified_changeset =
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,
field_type: &FieldType,
) -> TypeOptionData {
) -> Result<TypeOptionData, ProtobufError> {
let bytes = bytes.into();
let result: Result<TypeOptionData, ProtobufError> = match field_type {
match field_type {
FieldType::RichText => {
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 => {
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 {

View File

@ -64,7 +64,7 @@ impl DatabaseFieldTest {
match script {
FieldScript::CreateField { params } => {
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);
assert_eq!(self.field_count, fields.len());
},