feat: field editing bloc refactor and add mobile field editor (#3981)

This commit is contained in:
Richard Shiue 2023-11-23 16:43:29 +08:00 committed by GitHub
parent 8afbf28430
commit 66835a5409
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
49 changed files with 1002 additions and 1118 deletions

View File

@ -32,7 +32,7 @@ void main() {
await tester.pumpAndSettle();
});
// Makesure the text cells are filled with the right content when there are
// Make sure the text cells are filled with the right content when there are
// multiple text cell
testWidgets('edit multiple text cells', (tester) async {
await tester.initializeAppFlowy();

View File

@ -106,7 +106,7 @@ void main() {
await tester.pumpAndSettle();
});
testWidgets('create checklist field ', (tester) async {
testWidgets('create checklist field', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();

View File

@ -123,7 +123,6 @@ void main() {
await tester.tapTypeOptionButton();
await tester.selectFieldType(fieldType);
await tester.dismissFieldEditor();
// After update the field type, the cells should be updated
await tester.findCellByFieldType(fieldType);

View File

@ -21,7 +21,6 @@ import 'package:appflowy/plugins/database_view/grid/presentation/widgets/filter/
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/filter/filter_menu_item.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/footer/grid_footer.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/field_cell.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/field_cell_action_sheet.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/field_editor.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/field_type_extension.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/field_type_list.dart';
@ -688,7 +687,10 @@ extension AppFlowyDatabaseTest on WidgetTester {
}
Future<void> tapDeletePropertyInFieldEditor() async {
final deleteButton = find.byType(DeleteFieldButton);
final deleteButton = find.byWidgetPredicate(
(widget) =>
widget is FieldActionCell && widget.action == FieldAction.delete,
);
await tapButton(deleteButton);
final confirmButton = find.descendant(
@ -765,13 +767,18 @@ extension AppFlowyDatabaseTest on WidgetTester {
Future<void> tapHidePropertyButton() async {
final field = find.byWidgetPredicate(
(widget) =>
widget is FieldActionCell && widget.action == FieldAction.hide,
widget is FieldActionCell &&
widget.action == FieldAction.toggleVisibility,
);
await tapButton(field);
}
Future<void> tapHidePropertyButtonInFieldEditor() async {
final button = find.byType(FieldVisibilityToggleButton);
final button = find.byWidgetPredicate(
(widget) =>
widget is FieldActionCell &&
widget.action == FieldAction.toggleVisibility,
);
await tapButton(button);
}

View File

@ -0,0 +1,211 @@
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/type_option/type_option_context.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.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:go_router/go_router.dart';
import 'bottom_sheet_action_widget.dart';
import 'bottom_sheet_database_field_header.dart';
import 'bottom_sheet_rename_widget.dart';
/// The mobile bottom bar field editor is a two-deep menu. The type option
/// sub-menu may have its own sub-menus as well though.
enum MobileDBBottomSheetViewMode {
// operations shared between all fields
general,
// operations specific to the field type
typeOption,
}
class MobileDBBottomSheetFieldEditor extends StatefulWidget {
final String viewId;
final FieldController fieldController;
final FieldPB field;
final MobileDBBottomSheetViewMode initialPage;
const MobileDBBottomSheetFieldEditor({
super.key,
required this.viewId,
required this.fieldController,
required this.field,
this.initialPage = MobileDBBottomSheetViewMode.general,
});
@override
State<MobileDBBottomSheetFieldEditor> createState() =>
_MobileDBBottomSheetFieldEditorState();
}
class _MobileDBBottomSheetFieldEditorState
extends State<MobileDBBottomSheetFieldEditor> {
late MobileDBBottomSheetViewMode viewMode;
late final FieldEditorBloc _fieldEditorBloc;
@override
void initState() {
super.initState();
viewMode = widget.initialPage;
final loader = FieldTypeOptionLoader(
viewId: widget.viewId,
field: widget.field,
);
_fieldEditorBloc = FieldEditorBloc(
viewId: widget.viewId,
field: widget.field,
loader: loader,
fieldController: widget.fieldController,
)..add(const FieldEditorEvent.initial());
}
@override
Widget build(BuildContext context) {
return BlocProvider<FieldEditorBloc>.value(
value: _fieldEditorBloc,
child: Padding(
padding: const EdgeInsets.fromLTRB(16, 16, 16, 32),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
_buildHeader(),
const VSpace(16),
_buildBody(),
],
),
),
);
}
Widget _buildHeader() {
return MobileDBFieldBottomSheetHeader(
showBackButton: viewMode == MobileDBBottomSheetViewMode.typeOption,
onBack: () {
if (viewMode == MobileDBBottomSheetViewMode.typeOption) {
setState(() {
viewMode = MobileDBBottomSheetViewMode.general;
});
}
},
);
}
Widget _buildBody() {
return switch (viewMode) {
MobileDBBottomSheetViewMode.general => MobileDBFieldBottomSheetBody(
onAction: (action) {
switch (action) {
case MobileDBBottomSheetGeneralAction.typeOption:
break;
case MobileDBBottomSheetGeneralAction.toggleVisibility:
_fieldEditorBloc
.add(const FieldEditorEvent.toggleFieldVisibility());
context.pop();
break;
case MobileDBBottomSheetGeneralAction.delete:
_fieldEditorBloc.add(const FieldEditorEvent.deleteField());
context.pop();
break;
case MobileDBBottomSheetGeneralAction.duplicate:
_fieldEditorBloc.add(const FieldEditorEvent.duplicateField());
context.pop();
}
},
onRename: (name) {
_fieldEditorBloc.add(FieldEditorEvent.renameField(name));
},
),
MobileDBBottomSheetViewMode.typeOption => const SizedBox.shrink(),
};
}
}
enum MobileDBBottomSheetGeneralAction {
toggleVisibility,
duplicate,
delete,
typeOption,
}
class MobileDBFieldBottomSheetBody extends StatelessWidget {
const MobileDBFieldBottomSheetBody({
super.key,
required this.onAction,
required this.onRename,
});
final void Function(MobileDBBottomSheetGeneralAction action) onAction;
final void Function(String name) onRename;
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisSize: MainAxisSize.min,
children: [
// field name editor
MobileBottomSheetRenameWidget(
name: context.read<FieldEditorBloc>().state.field.name,
onRename: (newName) => onRename(newName),
padding: EdgeInsets.zero,
),
const VSpace(8),
// type option button
BottomSheetActionWidget(
svg: FlowySvgs.date_s,
text: LocaleKeys.grid_field_editProperty.tr(),
onTap: () {
onAction(MobileDBBottomSheetGeneralAction.typeOption);
},
),
const VSpace(8),
Row(
children: [
// hide/show field
Expanded(
child: BottomSheetActionWidget(
svg: FlowySvgs.hide_m,
text: LocaleKeys.grid_field_hide.tr(),
onTap: () {
onAction(MobileDBBottomSheetGeneralAction.toggleVisibility);
},
),
),
const HSpace(8),
// duplicate field
Expanded(
child: BottomSheetActionWidget(
svg: FlowySvgs.copy_s,
text: LocaleKeys.grid_field_duplicate.tr(),
onTap: () {
onAction(MobileDBBottomSheetGeneralAction.duplicate);
},
),
),
],
),
const VSpace(8),
Row(
children: [
// delete field
Expanded(
child: BottomSheetActionWidget(
svg: FlowySvgs.delete_s,
text: LocaleKeys.grid_field_delete.tr(),
onTap: () {
onAction(MobileDBBottomSheetGeneralAction.delete);
},
),
),
const HSpace(8),
const Expanded(child: SizedBox.shrink()),
],
),
],
);
}
}

View File

@ -0,0 +1,52 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
class MobileDBFieldBottomSheetHeader extends StatelessWidget {
const MobileDBFieldBottomSheetHeader({
super.key,
required this.showBackButton,
required this.onBack,
});
final bool showBackButton;
final VoidCallback onBack;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Row(
children: [
// back button
if (showBackButton)
InkWell(
onTap: onBack,
child: const Padding(
padding: EdgeInsets.symmetric(horizontal: 8.0),
child: Icon(
Icons.arrow_back_ios_new_rounded,
size: 24.0,
),
),
),
// field name
Expanded(
child: Text(
LocaleKeys.grid_field_editProperty.tr(),
style: theme.textTheme.labelSmall,
),
),
IconButton(
icon: Icon(
Icons.close,
color: theme.hintColor,
),
onPressed: () {
context.pop();
},
),
],
);
}
}

View File

@ -8,10 +8,12 @@ class MobileBottomSheetRenameWidget extends StatefulWidget {
super.key,
required this.name,
required this.onRename,
this.padding = const EdgeInsets.symmetric(horizontal: 12.0, vertical: 16.0),
});
final String name;
final void Function(String name) onRename;
final EdgeInsets padding;
@override
State<MobileBottomSheetRenameWidget> createState() =>
@ -39,17 +41,13 @@ class _MobileBottomSheetRenameWidgetState
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(
horizontal: 4.0,
vertical: 16.0,
),
padding: widget.padding,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const HSpace(8.0),
Expanded(
child: SizedBox(
height: 44.0,
height: 42.0,
child: FlowyTextField(
controller: controller,
),
@ -58,17 +56,16 @@ class _MobileBottomSheetRenameWidgetState
const HSpace(12.0),
FlowyTextButton(
LocaleKeys.button_edit.tr(),
constraints: const BoxConstraints.tightFor(height: 42),
padding: const EdgeInsets.symmetric(
vertical: 12.0,
horizontal: 16.0,
),
fontColor: Colors.white,
fillColor: Colors.lightBlue.shade300,
fillColor: Theme.of(context).primaryColor,
onPressed: () {
widget.onRename(controller.text);
},
),
const HSpace(8.0),
],
),
);

View File

@ -4,6 +4,7 @@ import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet_action_widget.dart';
import 'package:appflowy/mobile/presentation/widgets/show_flowy_mobile_bottom_sheet.dart';
import 'package:appflowy/plugins/database_view/application/cell/cell_service.dart';
import 'package:appflowy/plugins/database_view/application/field/field_controller.dart';
import 'package:appflowy/plugins/database_view/application/field/field_info.dart';
import 'package:appflowy/plugins/database_view/application/row/row_banner_bloc.dart';
import 'package:appflowy/plugins/database_view/application/row/row_controller.dart';
@ -24,12 +25,16 @@ class MobileCardDetailScreen extends StatefulWidget {
const MobileCardDetailScreen({
super.key,
required this.rowController,
required this.fieldController,
});
static const routeName = '/MobileCardDetailScreen';
static const argRowController = 'rowController';
static const argCellBuilder = 'cellBuilder';
static const argFieldController = 'fieldController';
final RowController rowController;
final FieldController fieldController;
@override
State<MobileCardDetailScreen> createState() => _MobileCardDetailScreenState();
@ -169,6 +174,7 @@ class _MobileCardDetailScreenState extends State<MobileCardDetailScreen> {
MobileRowPropertyList(
cellBuilder: _cellBuilder,
viewId: widget.rowController.viewId,
fieldController: widget.fieldController,
),
const Divider(),
const VSpace(16),

View File

@ -1,5 +1,6 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/database/card/card_property_edit/mobile_field_editor.dart';
import 'package:appflowy/plugins/database_view/application/field/field_controller.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:easy_localization/easy_localization.dart';
@ -9,15 +10,18 @@ import 'package:go_router/go_router.dart';
class MobileCreateRowFieldScreen extends StatefulWidget {
static const routeName = '/MobileCreateRowFieldScreen';
static const argViewId = 'viewId';
static const argFieldController = 'fieldController';
static const argTypeOption = 'typeOption';
const MobileCreateRowFieldScreen({
super.key,
required this.viewId,
required this.typeOption,
super.key,
required this.fieldController,
});
final String viewId;
final FieldController fieldController;
final TypeOptionPB typeOption;
@override
@ -53,6 +57,8 @@ class _MobileCreateRowFieldScreenState
viewId: widget.viewId,
field: widget.typeOption.field_2,
),
fieldController: widget.fieldController,
field: widget.typeOption.field_2,
),
);
}

View File

@ -1,6 +1,7 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/database/card/card_detail/mobile_create_row_field_screen.dart';
import 'package:appflowy/plugins/database_view/application/field/field_controller.dart';
import 'package:appflowy/plugins/database_view/application/field/type_option/type_option_service.dart';
import 'package:appflowy_backend/log.dart';
import 'package:easy_localization/easy_localization.dart';
@ -8,9 +9,14 @@ import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
class MobileCreateRowFieldButton extends StatelessWidget {
const MobileCreateRowFieldButton({super.key, required this.viewId});
const MobileCreateRowFieldButton({
super.key,
required this.viewId,
required this.fieldController,
});
final String viewId;
final FieldController fieldController;
@override
Widget build(BuildContext context) {
@ -32,6 +38,7 @@ class MobileCreateRowFieldButton extends StatelessWidget {
extra: {
MobileCreateRowFieldScreen.argViewId: viewId,
MobileCreateRowFieldScreen.argTypeOption: typeOption,
MobileCreateRowFieldScreen.argFieldController: fieldController,
},
);
},

View File

@ -38,7 +38,7 @@ class _MobileFieldNameTextFieldState extends State<MobileFieldNameTextField> {
onChanged: (newName) {
context
.read<FieldEditorBloc>()
.add(FieldEditorEvent.updateName(newName));
.add(FieldEditorEvent.renameField(newName));
},
);
}

View File

@ -3,6 +3,7 @@ import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/database/card/card_detail/widgets/mobile_create_row_field_button.dart';
import 'package:appflowy/mobile/presentation/database/card/card_property_edit/card_property_edit_screen.dart';
import 'package:appflowy/plugins/database_view/application/cell/cell_service.dart';
import 'package:appflowy/plugins/database_view/application/field/field_controller.dart';
import 'package:appflowy/plugins/database_view/grid/application/row/row_detail_bloc.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/field_type_extension.dart';
import 'package:appflowy/plugins/database_view/widgets/row/accessory/cell_accessory.dart';
@ -23,10 +24,12 @@ class MobileRowPropertyList extends StatelessWidget {
const MobileRowPropertyList({
super.key,
required this.viewId,
required this.fieldController,
required this.cellBuilder,
});
final String viewId;
final FieldController fieldController;
final GridCellBuilder cellBuilder;
@override
@ -44,6 +47,7 @@ class MobileRowPropertyList extends StatelessWidget {
itemBuilder: (context, index) => _PropertyCell(
key: ValueKey('row_detail_${visibleCells[index].fieldId}'),
cellContext: visibleCells[index],
fieldController: fieldController,
cellBuilder: cellBuilder,
index: index,
),
@ -75,6 +79,7 @@ class MobileRowPropertyList extends StatelessWidget {
// add new field
MobileCreateRowFieldButton(
viewId: viewId,
fieldController: fieldController,
),
],
),
@ -93,11 +98,13 @@ class _PropertyCell extends StatefulWidget {
const _PropertyCell({
super.key,
required this.cellContext,
required this.fieldController,
required this.cellBuilder,
required this.index,
});
final DatabaseCellContext cellContext;
final FieldController fieldController;
final GridCellBuilder cellBuilder;
final int index;
@ -142,6 +149,7 @@ class _PropertyCellState extends State<_PropertyCell> {
CardPropertyEditScreen.routeName,
extra: {
CardPropertyEditScreen.argCellContext: widget.cellContext,
CardPropertyEditScreen.argFieldController: widget.fieldController,
CardPropertyEditScreen.argRowDetailBloc:
context.read<RowDetailBloc>(),
},

View File

@ -3,6 +3,7 @@ import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/database/card/card_property_edit/mobile_field_editor.dart';
import 'package:appflowy/mobile/presentation/widgets/show_flowy_mobile_confirm_dialog.dart';
import 'package:appflowy/plugins/database_view/application/cell/cell_service.dart';
import 'package:appflowy/plugins/database_view/application/field/field_controller.dart';
import 'package:appflowy/plugins/database_view/application/field/type_option/type_option_context.dart';
import 'package:appflowy/plugins/database_view/grid/application/row/row_detail_bloc.dart';
import 'package:easy_localization/easy_localization.dart';
@ -14,13 +15,16 @@ class CardPropertyEditScreen extends StatelessWidget {
const CardPropertyEditScreen({
super.key,
required this.cellContext,
required this.fieldController,
});
static const routeName = '/CardPropertyEditScreen';
static const argCellContext = 'cellContext';
static const argFieldController = 'fieldController';
static const argRowDetailBloc = 'rowDetailBloc';
final DatabaseCellContext cellContext;
final FieldController fieldController;
@override
Widget build(BuildContext context) {
@ -57,7 +61,8 @@ class CardPropertyEditScreen extends StatelessWidget {
viewId: cellContext.viewId,
field: cellContext.fieldInfo.field,
),
fieldInfo: cellContext.fieldInfo,
fieldController: fieldController,
field: cellContext.fieldInfo.field,
),
);
}

View File

@ -2,12 +2,11 @@ import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/database/card/card_detail/widgets/widgets.dart';
import 'package:appflowy/mobile/presentation/database/card/card_property_edit/mobile_field_type_option_editor.dart';
import 'package:appflowy/mobile/presentation/database/card/card_property_edit/widgets/property_title.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/type_option/type_option_context.dart';
import 'package:appflowy/plugins/database_view/grid/application/row/row_detail_bloc.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/field_settings_entities.pbenum.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/widget/spacing.dart';
import 'package:flutter/material.dart';
@ -19,65 +18,63 @@ class MobileFieldEditor extends StatelessWidget {
super.key,
required this.viewId,
required this.typeOptionLoader,
this.isGroupingField = false,
this.fieldInfo,
required this.field,
required this.fieldController,
});
final String viewId;
final bool isGroupingField;
final FieldController fieldController;
final FieldTypeOptionLoader typeOptionLoader;
final FieldInfo? fieldInfo;
final FieldPB field;
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) {
return FieldEditorBloc(
// group field is the field to be used to group cards in database view, it can not be deleted
isGroupField: isGroupingField,
viewId: viewId,
loader: typeOptionLoader,
field: typeOptionLoader.field,
field: field,
fieldController: fieldController,
)..add(const FieldEditorEvent.initial());
},
child: BlocBuilder<FieldEditorBloc, FieldEditorState>(
builder: (context, state) {
// for field type edit option
final dataController = context.read<FieldEditorBloc>().dataController;
final dataController =
context.read<FieldEditorBloc>().typeOptionController;
return Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// property name
// field name
// TODO(yijing): improve hint text
PropertyTitle(LocaleKeys.settings_user_name.tr()),
BlocSelector<FieldEditorBloc, FieldEditorState, String>(
selector: (state) {
return state.name;
},
builder: (context, propertyName) {
selector: (state) => state.field.name,
builder: (context, fieldName) {
return MobileFieldNameTextField(
text: propertyName,
text: fieldName,
);
},
),
Row(
children: [
PropertyTitle(LocaleKeys.grid_field_visibility.tr()),
const Spacer(),
Expanded(
child:
PropertyTitle(LocaleKeys.grid_field_visibility.tr()),
),
VisibilitySwitch(
isFieldHidden:
fieldInfo?.visibility == FieldVisibility.AlwaysHidden,
isFieldHidden: state.field.visibility ==
FieldVisibility.AlwaysHidden,
onChanged: () {
state.field.fold(
() => Log.error('Can not hidden the field'),
(field) => context.read<RowDetailBloc>().add(
RowDetailEvent.toggleFieldVisibility(
field.id,
),
context.read<RowDetailBloc>().add(
RowDetailEvent.toggleFieldVisibility(
state.field.id,
),
);
);
},
),
],

View File

@ -1,108 +0,0 @@
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.pb.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'field_info.dart';
import 'field_service.dart';
part 'field_action_sheet_bloc.freezed.dart';
class FieldActionSheetBloc
extends Bloc<FieldActionSheetEvent, FieldActionSheetState> {
final String fieldId;
final FieldBackendService fieldService;
final FieldSettingsBackendService fieldSettingsService;
FieldActionSheetBloc({
required String viewId,
required FieldInfo fieldInfo,
}) : fieldId = fieldInfo.id,
fieldService = FieldBackendService(
viewId: viewId,
fieldId: fieldInfo.id,
),
fieldSettingsService = FieldSettingsBackendService(viewId: viewId),
super(
FieldActionSheetState.initial(
TypeOptionPB.create()..field_2 = fieldInfo.field,
),
) {
on<FieldActionSheetEvent>(
(event, emit) async {
await event.map(
updateFieldName: (_UpdateFieldName value) async {
final result = await fieldService.updateField(name: value.name);
result.fold(
(l) => null,
(err) => Log.error(err),
);
},
hideField: (_HideField value) async {
final result = await fieldSettingsService.updateFieldSettings(
fieldId: fieldId,
fieldVisibility: FieldVisibility.AlwaysHidden,
);
result.fold(
(l) => null,
(err) => Log.error(err),
);
},
showField: (_ShowField value) async {
final result = await fieldSettingsService.updateFieldSettings(
fieldId: fieldId,
fieldVisibility: FieldVisibility.AlwaysShown,
);
result.fold(
(l) => null,
(err) => Log.error(err),
);
},
deleteField: (_DeleteField value) async {
final result = await fieldService.deleteField();
result.fold(
(l) => null,
(err) => Log.error(err),
);
},
duplicateField: (_DuplicateField value) async {
final result = await fieldService.duplicateField();
result.fold(
(l) => null,
(err) => Log.error(err),
);
},
saveField: (_SaveField value) {},
);
},
);
}
}
@freezed
class FieldActionSheetEvent with _$FieldActionSheetEvent {
const factory FieldActionSheetEvent.updateFieldName(String name) =
_UpdateFieldName;
const factory FieldActionSheetEvent.hideField() = _HideField;
const factory FieldActionSheetEvent.showField() = _ShowField;
const factory FieldActionSheetEvent.duplicateField() = _DuplicateField;
const factory FieldActionSheetEvent.deleteField() = _DeleteField;
const factory FieldActionSheetEvent.saveField() = _SaveField;
}
@freezed
class FieldActionSheetState with _$FieldActionSheetState {
const factory FieldActionSheetState({
required TypeOptionPB fieldTypeOptionData,
required String errorText,
required String fieldName,
}) = _FieldActionSheetState;
factory FieldActionSheetState.initial(TypeOptionPB data) =>
FieldActionSheetState(
fieldTypeOptionData: data,
errorText: '',
fieldName: data.field_2.name,
);
}

View File

@ -1,112 +1,135 @@
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-error/errors.pb.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:dartz/dartz.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'field_controller.dart';
import 'field_info.dart';
import 'field_listener.dart';
import 'field_service.dart';
import 'type_option/type_option_context.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'type_option/type_option_data_controller.dart';
part 'field_editor_bloc.freezed.dart';
class FieldEditorBloc extends Bloc<FieldEditorEvent, FieldEditorState> {
final TypeOptionController dataController;
final FieldPB field;
final String viewId;
final FieldController fieldController;
final SingleFieldListener _singleFieldListener;
final FieldBackendService fieldService;
final FieldSettingsBackendService fieldSettingsService;
final TypeOptionController typeOptionController;
FieldEditorBloc({
required bool isGroupField,
required FieldPB field,
required this.viewId,
required this.field,
required this.fieldController,
required FieldTypeOptionLoader loader,
}) : dataController = TypeOptionController(
}) : typeOptionController = TypeOptionController(
field: field,
loader: loader,
),
super(
FieldEditorState.initial(
loader.viewId,
loader.field.name,
isGroupField,
),
) {
_singleFieldListener = SingleFieldListener(fieldId: field.id),
fieldService = FieldBackendService(
viewId: viewId,
fieldId: field.id,
),
fieldSettingsService = FieldSettingsBackendService(viewId: viewId),
super(FieldEditorState(field: FieldInfo.initial(field))) {
on<FieldEditorEvent>(
(event, emit) async {
await event.when(
initial: () async {
dataController.addFieldListener((field) {
final fieldId = field.id;
typeOptionController.addFieldListener((field) {
if (!isClosed) {
add(FieldEditorEvent.didReceiveFieldChanged(field));
add(FieldEditorEvent.didReceiveFieldChanged(fieldId));
}
});
await dataController.reloadTypeOption();
add(FieldEditorEvent.didReceiveFieldChanged(dataController.field));
},
updateName: (name) {
if (state.name != name) {
dataController.fieldName = name;
emit(state.copyWith(name: name));
}
},
didReceiveFieldChanged: (FieldPB field) {
emit(
state.copyWith(
field: Some(field),
name: field.name,
canDelete: field.isPrimary,
),
);
},
deleteField: () {
state.field.fold(
() => null,
(field) {
final fieldService = FieldBackendService(
viewId: loader.viewId,
fieldId: field.id,
);
fieldService.deleteField();
_singleFieldListener.start(
onFieldChanged: (field) {
if (!isClosed) {
add(FieldEditorEvent.didReceiveFieldChanged(fieldId));
}
},
);
await typeOptionController.reloadTypeOption();
add(FieldEditorEvent.didReceiveFieldChanged(fieldId));
},
switchToField: (FieldType fieldType) async {
await dataController.switchToField(fieldType);
didReceiveFieldChanged: (fieldId) async {
await Future.delayed(const Duration(milliseconds: 50));
emit(state.copyWith(field: fieldController.getField(fieldId)!));
},
switchFieldType: (fieldType) async {
await typeOptionController.switchToField(fieldType);
},
renameField: (newName) async {
final result = await fieldService.updateField(name: newName);
_logIfError(result);
},
toggleFieldVisibility: () async {
final currentVisibility =
state.field.visibility ?? FieldVisibility.AlwaysShown;
final newVisibility =
currentVisibility == FieldVisibility.AlwaysHidden
? FieldVisibility.AlwaysShown
: FieldVisibility.AlwaysHidden;
final result = await fieldSettingsService.updateFieldSettings(
fieldId: state.field.id,
fieldVisibility: newVisibility,
);
_logIfError(result);
},
deleteField: () async {
final result = await fieldService.deleteField();
_logIfError(result);
},
duplicateField: () async {
final result = await fieldService.duplicateField();
_logIfError(result);
},
);
},
);
}
void _logIfError(Either<Unit, FlowyError> result) {
result.fold(
(l) => null,
(err) => Log.error(err),
);
}
@override
Future<void> close() {
_singleFieldListener.stop();
return super.close();
}
}
@freezed
class FieldEditorEvent with _$FieldEditorEvent {
const factory FieldEditorEvent.initial() = _InitialField;
const factory FieldEditorEvent.updateName(String name) = _UpdateName;
const factory FieldEditorEvent.deleteField() = _DeleteField;
const factory FieldEditorEvent.switchToField(FieldType fieldType) =
_SwitchToField;
const factory FieldEditorEvent.didReceiveFieldChanged(FieldPB field) =
const factory FieldEditorEvent.didReceiveFieldChanged(String fieldId) =
_DidReceiveFieldChanged;
const factory FieldEditorEvent.switchFieldType(FieldType fieldType) =
_SwitchFieldType;
const factory FieldEditorEvent.renameField(String name) = _RenameField;
const factory FieldEditorEvent.toggleFieldVisibility() =
_ToggleFieldVisiblity;
const factory FieldEditorEvent.deleteField() = _DeleteField;
const factory FieldEditorEvent.duplicateField() = _DuplicateField;
}
@freezed
class FieldEditorState with _$FieldEditorState {
const factory FieldEditorState({
required String viewId,
required String errorText,
required String name,
required Option<FieldPB> field,
required bool canDelete,
required bool isGroupField,
required FieldInfo field,
}) = _FieldEditorState;
factory FieldEditorState.initial(
String viewId,
String fieldName,
bool isGroupField,
) =>
FieldEditorState(
viewId: viewId,
errorText: '',
field: none(),
canDelete: false,
name: fieldName,
isGroupField: isGroupField,
);
}

View File

@ -27,7 +27,6 @@ class FieldBackendService {
Future<Either<Unit, FlowyError>> updateField({
String? name,
bool? frozen,
double? width,
}) {
final payload = FieldChangesetPB.create()
..viewId = viewId
@ -41,10 +40,6 @@ class FieldBackendService {
payload.frozen = frozen;
}
if (width != null) {
payload.width = width.toInt();
}
return DatabaseEventUpdateField(payload).send();
}

View File

@ -1,4 +1,5 @@
import 'dart:collection';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/database/card/card.dart';
@ -16,7 +17,6 @@ import 'package:appflowy_backend/protobuf/flowy-database2/row_entities.pb.dart';
import 'package:appflowy_board/appflowy_board.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flowy_infra_ui/widget/error_page.dart';
import 'package:flowy_infra_ui/widget/flowy_tooltip.dart';
import 'package:flutter/material.dart' hide Card;
@ -326,13 +326,15 @@ class _BoardContentState extends State<BoardContent> {
context.push(
MobileCardDetailScreen.routeName,
extra: {
'rowController': dataController,
MobileCardDetailScreen.argRowController: dataController,
MobileCardDetailScreen.argFieldController: fieldController,
},
);
} else {
FlowyOverlay.show(
context: context,
builder: (_) => RowDetailPage(
fieldController: fieldController,
cellBuilder: GridCellBuilder(cellCache: dataController.cellCache),
rowController: dataController,
),

View File

@ -421,6 +421,8 @@ class HiddenGroupPopupItemList extends StatelessWidget {
context: context,
builder: (BuildContext context) {
return RowDetailPage(
fieldController:
context.read<BoardBloc>().fieldController,
cellBuilder: GridCellBuilder(
cellCache: rowController.cellCache,
),

View File

@ -297,6 +297,7 @@ class _EventList extends StatelessWidget {
final autoEdit =
editingEvent?.event?.eventId == events[index].eventId;
return EventCard(
fieldController: context.read<CalendarBloc>().fieldController,
event: events[index],
viewId: viewId,
rowCache: rowCache,

View File

@ -1,4 +1,5 @@
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/row/row_cache.dart';
import 'package:appflowy/plugins/database_view/widgets/card/card.dart';
import 'package:appflowy/plugins/database_view/widgets/card/card_cell_builder.dart';
@ -20,6 +21,7 @@ import '../application/calendar_bloc.dart';
import 'calendar_event_editor.dart';
class EventCard extends StatefulWidget {
final FieldController fieldController;
final CalendarDayEvent event;
final String viewId;
final RowCache rowCache;
@ -27,12 +29,13 @@ class EventCard extends StatefulWidget {
final bool autoEdit;
const EventCard({
super.key,
required this.event,
required this.viewId,
required this.rowCache,
required this.constraints,
required this.autoEdit,
super.key,
required this.fieldController,
});
@override
@ -164,6 +167,7 @@ class _EventCardState extends State<EventCard> {
rowMeta: widget.event.event.rowMeta,
viewId: widget.viewId,
layoutSettings: settings,
fieldController: widget.fieldController,
);
},
child: DecoratedBox(

View File

@ -1,6 +1,7 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/database_view/application/cell/cell_service.dart';
import 'package:appflowy/plugins/database_view/application/field/field_controller.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/calendar/application/calendar_event_editor_bloc.dart';
@ -19,6 +20,7 @@ import 'package:flutter_bloc/flutter_bloc.dart';
class CalendarEventEditor extends StatelessWidget {
final RowController rowController;
final FieldController fieldController;
final CalendarLayoutSettingPB layoutSettings;
final GridCellBuilder cellBuilder;
@ -28,6 +30,7 @@ class CalendarEventEditor extends StatelessWidget {
required RowMetaPB rowMeta,
required String viewId,
required this.layoutSettings,
required this.fieldController,
}) : rowController = RowController(
rowMeta: rowMeta,
viewId: viewId,
@ -45,7 +48,10 @@ class CalendarEventEditor extends StatelessWidget {
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
EventEditorControls(rowController: rowController),
EventEditorControls(
rowController: rowController,
fieldController: fieldController,
),
Flexible(
child: EventPropertyList(
dateFieldId: layoutSettings.fieldId,
@ -60,9 +66,11 @@ class CalendarEventEditor extends StatelessWidget {
class EventEditorControls extends StatelessWidget {
final RowController rowController;
final FieldController fieldController;
const EventEditorControls({
super.key,
required this.rowController,
required this.fieldController,
});
@override
@ -91,6 +99,7 @@ class EventEditorControls extends StatelessWidget {
context: context,
builder: (BuildContext context) {
return RowDetailPage(
fieldController: fieldController,
cellBuilder: GridCellBuilder(
cellCache: rowController.cellCache,
),

View File

@ -1,6 +1,7 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/database_view/application/database_controller.dart';
import 'package:appflowy/plugins/database_view/application/field/field_controller.dart';
import 'package:appflowy/plugins/database_view/calendar/application/calendar_bloc.dart';
import 'package:appflowy/plugins/database_view/calendar/application/unschedule_event_bloc.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/layout/sizes.dart';
@ -304,6 +305,7 @@ void showEventDetails({
required CalendarEventPB event,
required String viewId,
required RowCache rowCache,
required FieldController fieldController,
}) {
final dataController = RowController(
rowMeta: event.rowMeta,
@ -313,12 +315,13 @@ void showEventDetails({
FlowyOverlay.show(
context: context,
builder: (BuildContext context) {
builder: (BuildContext overlayContext) {
return RowDetailPage(
cellBuilder: GridCellBuilder(
cellCache: rowCache.cellCache,
),
rowController: dataController,
fieldController: fieldController,
);
},
);
@ -387,8 +390,7 @@ class _UnscheduledEventsButtonState extends State<UnscheduledEventsButton> {
),
popupBuilder: (context) {
return UnscheduleEventsList(
viewId: widget.databaseController.viewId,
rowCache: widget.databaseController.rowCache,
databaseController: widget.databaseController,
unscheduleEvents: state.unscheduleEvents,
);
},
@ -400,14 +402,12 @@ class _UnscheduledEventsButtonState extends State<UnscheduledEventsButton> {
}
class UnscheduleEventsList extends StatelessWidget {
final String viewId;
final RowCache rowCache;
final DatabaseController databaseController;
final List<CalendarEventPB> unscheduleEvents;
const UnscheduleEventsList({
required this.viewId,
required this.unscheduleEvents,
required this.rowCache,
super.key,
required this.unscheduleEvents,
required this.databaseController,
});
@override
@ -429,8 +429,9 @@ class UnscheduleEventsList extends StatelessWidget {
showEventDetails(
context: context,
event: e,
viewId: viewId,
rowCache: rowCache,
viewId: databaseController.viewId,
rowCache: databaseController.rowCache,
fieldController: databaseController.fieldController,
);
PopoverContainer.of(context).close();
},

View File

@ -202,8 +202,6 @@ class _GridHeader extends StatelessWidget {
builder: (context, state) {
return GridHeaderSliverAdaptor(
viewId: state.viewId,
fieldController:
context.read<GridBloc>().databaseController.fieldController,
anchorScrollController: headerScrollController,
);
},
@ -360,6 +358,7 @@ class _GridRows extends StatelessWidget {
return RowDetailPage(
cellBuilder: cellBuilder,
rowController: dataController,
fieldController: fieldController,
);
},
);

View File

@ -11,7 +11,7 @@ class GridSize {
static double get headerContainerPadding => 0 * scale;
static double get cellHPadding => 10 * scale;
static double get cellVPadding => 10 * scale;
static double get popoverItemHeight => 32 * scale;
static double get popoverItemHeight => 26 * scale;
static double get typeOptionSeparatorHeight => 4 * scale;
static EdgeInsets get headerContentInsets => EdgeInsets.symmetric(

View File

@ -184,8 +184,6 @@ class _GridHeader extends StatelessWidget {
builder: (context, state) {
return GridHeaderSliverAdaptor(
viewId: state.viewId,
fieldController:
context.read<GridBloc>().databaseController.fieldController,
anchorScrollController: headerScrollController,
);
},
@ -363,6 +361,7 @@ class _GridRows extends StatelessWidget {
return RowDetailPage(
cellBuilder: cellBuilder,
rowController: dataController,
fieldController: fieldController,
);
},
);

View File

@ -1,5 +1,6 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/plugins/database_view/application/field/field_cell_bloc.dart';
import 'package:appflowy/plugins/database_view/application/field/field_controller.dart';
import 'package:appflowy/plugins/database_view/application/field/field_info.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
@ -11,15 +12,17 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../layout/sizes.dart';
import 'field_cell_action_sheet.dart';
import 'field_editor.dart';
import 'field_type_extension.dart';
class GridFieldCell extends StatefulWidget {
final String viewId;
final FieldController fieldController;
final FieldInfo fieldInfo;
const GridFieldCell({
super.key,
required this.viewId,
required this.fieldController,
required this.fieldInfo,
});
@ -59,9 +62,11 @@ class _GridFieldCellState extends State<GridFieldCell> {
direction: PopoverDirection.bottomWithLeftAligned,
controller: popoverController,
popupBuilder: (BuildContext context) {
return GridFieldCellActionSheet(
return FieldEditor(
viewId: widget.viewId,
fieldInfo: widget.fieldInfo,
fieldController: widget.fieldController,
field: widget.fieldInfo.field,
initialPage: FieldEditorPage.general,
);
},
child: FieldCellButton(

View File

@ -1,266 +0,0 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/plugins/database_view/application/field/field_action_sheet_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/workspace/presentation/widgets/dialogs.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra_ui/style_widget/button.dart';
import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flowy_infra_ui/widget/spacing.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 'package:styled_widget/styled_widget.dart';
import '../../layout/sizes.dart';
import 'field_editor.dart';
class GridFieldCellActionSheet extends StatefulWidget {
final String viewId;
final FieldInfo fieldInfo;
const GridFieldCellActionSheet({
required this.viewId,
required this.fieldInfo,
Key? key,
}) : super(key: key);
@override
State<StatefulWidget> createState() => _GridFieldCellActionSheetState();
}
class _GridFieldCellActionSheetState extends State<GridFieldCellActionSheet> {
bool _showFieldEditor = false;
@override
Widget build(BuildContext context) {
if (_showFieldEditor) {
return SizedBox(
width: 400,
child: FieldEditor(
viewId: widget.viewId,
fieldInfo: widget.fieldInfo,
typeOptionLoader: FieldTypeOptionLoader(
viewId: widget.viewId,
field: widget.fieldInfo.field,
),
),
);
}
return BlocProvider(
create: (context) => FieldActionSheetBloc(
viewId: widget.viewId,
fieldInfo: widget.fieldInfo,
),
child: IntrinsicWidth(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
_EditFieldButton(
onTap: () {
setState(() => _showFieldEditor = true);
},
),
VSpace(GridSize.typeOptionSeparatorHeight),
_FieldOperationList(
viewId: widget.viewId,
fieldInfo: widget.fieldInfo,
),
],
),
),
).padding(all: 6.0);
}
}
class _EditFieldButton extends StatelessWidget {
final void Function()? onTap;
const _EditFieldButton({Key? key, this.onTap}) : super(key: key);
@override
Widget build(BuildContext context) {
return BlocBuilder<FieldActionSheetBloc, FieldActionSheetState>(
builder: (context, state) {
return SizedBox(
height: GridSize.popoverItemHeight,
child: FlowyButton(
hoverColor: AFThemeExtension.of(context).lightGreyHover,
text: FlowyText.medium(
LocaleKeys.grid_field_editProperty.tr(),
color: AFThemeExtension.of(context).textColor,
),
onTap: onTap,
),
);
},
);
}
}
class _FieldOperationList extends StatelessWidget {
final String viewId;
final FieldInfo fieldInfo;
const _FieldOperationList({
required this.viewId,
required this.fieldInfo,
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Column(
children: [
Flex(
direction: Axis.horizontal,
children: [
_actionCell(FieldAction.hide),
HSpace(GridSize.typeOptionSeparatorHeight),
_actionCell(FieldAction.duplicate),
],
),
VSpace(GridSize.typeOptionSeparatorHeight),
Flex(
direction: Axis.horizontal,
children: [
_actionCell(FieldAction.delete),
HSpace(GridSize.typeOptionSeparatorHeight),
const Spacer(),
],
),
],
);
}
Widget _actionCell(FieldAction action) {
bool enable = true;
// If the field is primary, delete and duplicate are disabled.
if (fieldInfo.isPrimary) {
switch (action) {
case FieldAction.hide:
break;
case FieldAction.duplicate:
enable = false;
break;
case FieldAction.delete:
enable = false;
break;
}
}
return Flexible(
child: SizedBox(
height: GridSize.popoverItemHeight,
child: FieldActionCell(
viewId: viewId,
fieldInfo: fieldInfo,
action: action,
enable: enable,
),
),
);
}
}
class FieldActionCell extends StatelessWidget {
final String viewId;
final FieldInfo fieldInfo;
final FieldAction action;
final bool enable;
const FieldActionCell({
required this.viewId,
required this.fieldInfo,
required this.action,
required this.enable,
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return FlowyButton(
hoverColor: AFThemeExtension.of(context).lightGreyHover,
disable: !enable,
text: FlowyText.medium(
action.title(),
color: enable
? AFThemeExtension.of(context).textColor
: Theme.of(context).disabledColor,
),
onTap: () => action.run(context, viewId, fieldInfo),
leftIcon: FlowySvg(
action.icon(),
color: enable
? AFThemeExtension.of(context).textColor
: Theme.of(context).disabledColor,
),
);
}
}
enum FieldAction {
hide,
duplicate,
delete,
}
extension _FieldActionExtension on FieldAction {
FlowySvgData icon() {
switch (this) {
case FieldAction.hide:
return FlowySvgs.hide_s;
case FieldAction.duplicate:
return FlowySvgs.copy_s;
case FieldAction.delete:
return FlowySvgs.delete_s;
}
}
String title() {
switch (this) {
case FieldAction.hide:
return LocaleKeys.grid_field_hide.tr();
case FieldAction.duplicate:
return LocaleKeys.grid_field_duplicate.tr();
case FieldAction.delete:
return LocaleKeys.grid_field_delete.tr();
}
}
void run(BuildContext context, String viewId, FieldInfo fieldInfo) {
switch (this) {
case FieldAction.hide:
context
.read<FieldActionSheetBloc>()
.add(const FieldActionSheetEvent.hideField());
break;
case FieldAction.duplicate:
PopoverContainer.of(context).close();
FieldBackendService(
viewId: viewId,
fieldId: fieldInfo.id,
).duplicateField();
break;
case FieldAction.delete:
PopoverContainer.of(context).close();
NavigatorAlertDialog(
title: LocaleKeys.grid_field_deleteFieldPromptMessage.tr(),
confirm: () {
FieldBackendService(
viewId: viewId,
fieldId: fieldInfo.field.id,
).deleteField();
},
).show(context);
break;
}
}
}

View File

@ -1,38 +1,41 @@
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_backend/protobuf/flowy-database2/field_settings_entities.pb.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/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.pbenum.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/style_widget/button.dart';
import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flowy_infra_ui/style_widget/text_field.dart';
import 'package:flowy_infra_ui/widget/spacing.dart';
import 'package:appflowy_backend/log.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import '../../layout/sizes.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:styled_widget/styled_widget.dart';
import 'field_type_option_editor.dart';
enum FieldEditorPage {
general,
details,
}
class FieldEditor extends StatefulWidget {
final String viewId;
final bool isGroupingField;
final Function(String)? onDeleted;
final Function(String)? onToggleVisibility;
final FieldTypeOptionLoader typeOptionLoader;
final FieldInfo? fieldInfo;
final FieldController fieldController;
final FieldPB field;
final FieldEditorPage initialPage;
const FieldEditor({
required this.viewId,
required this.typeOptionLoader,
this.fieldInfo,
this.isGroupingField = false,
this.onDeleted,
this.onToggleVisibility,
super.key,
required this.viewId,
required this.field,
required this.fieldController,
this.initialPage = FieldEditorPage.details,
});
@override
@ -40,6 +43,227 @@ class FieldEditor extends StatefulWidget {
}
class _FieldEditorState extends State<FieldEditor> {
late FieldEditorPage _currentPage;
late final TextEditingController textController;
@override
void initState() {
super.initState();
_currentPage = widget.initialPage;
textController = TextEditingController(text: widget.field.name);
}
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (_) => FieldEditorBloc(
viewId: widget.viewId,
field: widget.field,
fieldController: widget.fieldController,
loader: FieldTypeOptionLoader(
viewId: widget.viewId,
field: widget.field,
),
)..add(const FieldEditorEvent.initial()),
child: _currentPage == FieldEditorPage.details
? _fieldDetails()
: _fieldGeneral(),
);
}
Widget _fieldGeneral() {
return SizedBox(
width: 240,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
FieldNameTextField(
padding: const EdgeInsets.fromLTRB(4, 4, 4, 8),
textEditingController: textController,
),
_EditFieldButton(
onTap: () {
setState(() => _currentPage = FieldEditorPage.details);
},
),
VSpace(GridSize.typeOptionSeparatorHeight),
_actionCell(FieldAction.toggleVisibility),
VSpace(GridSize.typeOptionSeparatorHeight),
_actionCell(FieldAction.duplicate),
VSpace(GridSize.typeOptionSeparatorHeight),
_actionCell(FieldAction.delete),
],
).padding(all: 8.0),
);
}
Widget _fieldDetails() {
return SizedBox(
width: 260,
child: FieldDetailsEditor(
viewId: widget.viewId,
textEditingController: textController,
),
);
}
Widget _actionCell(FieldAction action) {
return BlocBuilder<FieldEditorBloc, FieldEditorState>(
builder: (context, state) => FieldActionCell(
viewId: widget.viewId,
fieldInfo: state.field,
action: action,
),
);
}
}
class _EditFieldButton extends StatelessWidget {
final void Function()? onTap;
const _EditFieldButton({this.onTap});
@override
Widget build(BuildContext context) {
return SizedBox(
height: GridSize.popoverItemHeight,
child: FlowyButton(
leftIcon: const FlowySvg(FlowySvgs.edit_s),
text: FlowyText.medium(
LocaleKeys.grid_field_editProperty.tr(),
),
onTap: onTap,
),
);
}
}
class FieldActionCell extends StatelessWidget {
final String viewId;
final FieldInfo fieldInfo;
final FieldAction action;
final PopoverMutex? popoverMutex;
const FieldActionCell({
super.key,
required this.viewId,
required this.fieldInfo,
required this.action,
this.popoverMutex,
});
@override
Widget build(BuildContext context) {
bool enable = true;
// If the field is primary, delete and duplicate are disabled.
if (fieldInfo.isPrimary &&
(action == FieldAction.duplicate || action == FieldAction.delete)) {
enable = false;
}
return FlowyButton(
disable: !enable,
text: FlowyText.medium(
action.title(fieldInfo),
color: enable ? null : Theme.of(context).disabledColor,
),
onHover: (_) => popoverMutex?.close(),
onTap: () => action.run(context, viewId, fieldInfo),
leftIcon: FlowySvg(
action.icon(fieldInfo),
size: const Size.square(16),
color: enable ? null : Theme.of(context).disabledColor,
),
);
}
}
enum FieldAction {
toggleVisibility,
duplicate,
delete,
}
extension _FieldActionExtension on FieldAction {
FlowySvgData icon(FieldInfo fieldInfo) {
switch (this) {
case FieldAction.toggleVisibility:
if (fieldInfo.visibility != null &&
fieldInfo.visibility == FieldVisibility.AlwaysHidden) {
return FlowySvgs.show_m;
} else {
return FlowySvgs.hide_s;
}
case FieldAction.duplicate:
return FlowySvgs.copy_s;
case FieldAction.delete:
return FlowySvgs.delete_s;
}
}
String title(FieldInfo fieldInfo) {
switch (this) {
case FieldAction.toggleVisibility:
if (fieldInfo.visibility != null &&
fieldInfo.visibility == FieldVisibility.AlwaysHidden) {
return LocaleKeys.grid_field_show.tr();
} else {
return LocaleKeys.grid_field_hide.tr();
}
case FieldAction.duplicate:
return LocaleKeys.grid_field_duplicate.tr();
case FieldAction.delete:
return LocaleKeys.grid_field_delete.tr();
}
}
void run(BuildContext context, String viewId, FieldInfo fieldInfo) {
switch (this) {
case FieldAction.toggleVisibility:
PopoverContainer.of(context).close();
context
.read<FieldEditorBloc>()
.add(const FieldEditorEvent.toggleFieldVisibility());
break;
case FieldAction.duplicate:
PopoverContainer.of(context).close();
context
.read<FieldEditorBloc>()
.add(const FieldEditorEvent.duplicateField());
break;
case FieldAction.delete:
NavigatorAlertDialog(
title: LocaleKeys.grid_field_deleteFieldPromptMessage.tr(),
confirm: () {
FieldBackendService(
viewId: viewId,
fieldId: fieldInfo.id,
).deleteField();
},
).show(context);
PopoverContainer.of(context).close();
break;
}
}
}
class FieldDetailsEditor extends StatefulWidget {
final String viewId;
final TextEditingController textEditingController;
final Function()? onAction;
const FieldDetailsEditor({
super.key,
required this.viewId,
required this.textEditingController,
this.onAction,
});
@override
State<StatefulWidget> createState() => _FieldDetailsEditorState();
}
class _FieldDetailsEditorState extends State<FieldDetailsEditor> {
late PopoverMutex popoverMutex;
@override
@ -56,71 +280,77 @@ class _FieldEditorState extends State<FieldEditor> {
@override
Widget build(BuildContext context) {
final bool requireSpace = widget.onDeleted != null ||
widget.onToggleVisibility != null ||
!widget.typeOptionLoader.field.isPrimary;
final List<Widget> children = [
FieldNameTextField(popoverMutex: popoverMutex),
if (requireSpace) const VSpace(4),
if (widget.onDeleted != null) _addDeleteFieldButton(),
if (widget.onToggleVisibility != null) _addHideFieldButton(),
if (!widget.typeOptionLoader.field.isPrimary)
FieldTypeOptionCell(popoverMutex: popoverMutex),
FieldNameTextField(
popoverMutex: popoverMutex,
padding: const EdgeInsets.fromLTRB(12.0, 4.0, 12.0, 0.0),
textEditingController: widget.textEditingController,
),
const VSpace(8),
FieldTypeOptionCell(popoverMutex: popoverMutex),
const TypeOptionSeparator(),
_addFieldVisibilityToggleButton(),
_addDuplicateFieldButton(),
_addDeleteFieldButton(),
];
return BlocProvider(
create: (context) {
return FieldEditorBloc(
isGroupField: widget.isGroupingField,
loader: widget.typeOptionLoader,
field: widget.typeOptionLoader.field,
)..add(const FieldEditorEvent.initial());
},
child: ListView.separated(
shrinkWrap: true,
itemCount: children.length,
itemBuilder: (context, index) => children[index],
separatorBuilder: (context, index) =>
VSpace(GridSize.typeOptionSeparatorHeight),
padding: const EdgeInsets.symmetric(vertical: 12.0),
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: Column(
mainAxisSize: MainAxisSize.min,
children: children,
),
);
}
Widget _addDeleteFieldButton() {
Widget _addFieldVisibilityToggleButton() {
return BlocBuilder<FieldEditorBloc, FieldEditorState>(
builder: (context, state) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 12.0),
child: DeleteFieldButton(
padding: const EdgeInsets.fromLTRB(8.0, 2.0, 8.0, 0),
child: FieldActionCell(
viewId: widget.viewId,
fieldInfo: state.field,
action: FieldAction.toggleVisibility,
popoverMutex: popoverMutex,
onDeleted: () {
state.field.fold(
() => Log.error('Can not delete the field'),
(field) => widget.onDeleted?.call(field.id),
);
},
),
);
},
);
}
Widget _addHideFieldButton() {
Widget _addDeleteFieldButton() {
return BlocBuilder<FieldEditorBloc, FieldEditorState>(
builder: (context, state) {
if (state.field.isPrimary) {
return const SizedBox.shrink();
}
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 12.0),
child: FieldVisibilityToggleButton(
isFieldHidden:
widget.fieldInfo!.visibility == FieldVisibility.AlwaysHidden,
padding: const EdgeInsets.fromLTRB(8.0, 4.0, 8.0, 0),
child: FieldActionCell(
viewId: widget.viewId,
fieldInfo: state.field,
action: FieldAction.delete,
popoverMutex: popoverMutex,
),
);
},
);
}
Widget _addDuplicateFieldButton() {
return BlocBuilder<FieldEditorBloc, FieldEditorState>(
builder: (context, state) {
if (state.field.isPrimary) {
return const SizedBox.shrink();
}
return Padding(
padding: const EdgeInsets.fromLTRB(8.0, 4.0, 8.0, 0),
child: FieldActionCell(
viewId: widget.viewId,
fieldInfo: state.field,
action: FieldAction.duplicate,
popoverMutex: popoverMutex,
onTap: () {
state.field.fold(
() => Log.error('Can not hidden the field'),
(field) => widget.onToggleVisibility?.call(field.id),
);
},
),
);
},
@ -132,25 +362,25 @@ class FieldTypeOptionCell extends StatelessWidget {
final PopoverMutex popoverMutex;
const FieldTypeOptionCell({
Key? key,
super.key,
required this.popoverMutex,
}) : super(key: key);
});
@override
Widget build(BuildContext context) {
return BlocBuilder<FieldEditorBloc, FieldEditorState>(
buildWhen: (p, c) => p.field != c.field,
builder: (context, state) {
return state.field.fold(
() => const SizedBox.shrink(),
(fieldInfo) {
final dataController =
context.read<FieldEditorBloc>().dataController;
return FieldTypeOptionEditor(
dataController: dataController,
popoverMutex: popoverMutex,
);
},
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,
),
);
},
);
@ -158,18 +388,21 @@ class FieldTypeOptionCell extends StatelessWidget {
}
class FieldNameTextField extends StatefulWidget {
final PopoverMutex popoverMutex;
final TextEditingController textEditingController;
final PopoverMutex? popoverMutex;
final EdgeInsets padding;
const FieldNameTextField({
required this.popoverMutex,
Key? key,
}) : super(key: key);
super.key,
required this.textEditingController,
this.popoverMutex,
this.padding = EdgeInsets.zero,
});
@override
State<FieldNameTextField> createState() => _FieldNameTextFieldState();
}
class _FieldNameTextFieldState extends State<FieldNameTextField> {
final textController = TextEditingController();
FocusNode focusNode = FocusNode();
@override
@ -178,11 +411,11 @@ class _FieldNameTextFieldState extends State<FieldNameTextField> {
focusNode.addListener(() {
if (focusNode.hasFocus) {
widget.popoverMutex.close();
widget.popoverMutex?.close();
}
});
widget.popoverMutex.listenOnPopoverChanged(() {
widget.popoverMutex?.listenOnPopoverChanged(() {
if (focusNode.hasFocus) {
focusNode.unfocus();
}
@ -191,24 +424,18 @@ class _FieldNameTextFieldState extends State<FieldNameTextField> {
@override
Widget build(BuildContext context) {
return BlocBuilder<FieldEditorBloc, FieldEditorState>(
builder: (context, state) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 12.0),
child: FlowyTextField(
focusNode: focusNode,
controller: textController,
onSubmitted: (String _) => PopoverContainer.of(context).close(),
text: state.name,
errorText: state.errorText.isEmpty ? null : state.errorText,
onChanged: (newName) {
context
.read<FieldEditorBloc>()
.add(FieldEditorEvent.updateName(newName));
},
),
);
},
return Padding(
padding: widget.padding,
child: FlowyTextField(
focusNode: focusNode,
controller: widget.textEditingController,
onSubmitted: (_) => PopoverContainer.of(context).close(),
onChanged: (newName) {
context
.read<FieldEditorBloc>()
.add(FieldEditorEvent.renameField(newName));
},
),
);
}
@ -216,80 +443,10 @@ class _FieldNameTextFieldState extends State<FieldNameTextField> {
void dispose() {
focusNode.removeListener(() {
if (focusNode.hasFocus) {
widget.popoverMutex.close();
widget.popoverMutex?.close();
}
});
focusNode.dispose();
super.dispose();
}
}
@visibleForTesting
class DeleteFieldButton extends StatelessWidget {
final PopoverMutex popoverMutex;
final VoidCallback? onDeleted;
const DeleteFieldButton({
required this.popoverMutex,
required this.onDeleted,
super.key,
});
@override
Widget build(BuildContext context) {
return BlocBuilder<FieldEditorBloc, FieldEditorState>(
buildWhen: (previous, current) => previous != current,
builder: (context, state) {
final enable = !state.canDelete && !state.isGroupField;
final Widget button = FlowyButton(
disable: !enable,
text: FlowyText.medium(
LocaleKeys.grid_field_delete.tr(),
color: enable ? null : Theme.of(context).disabledColor,
),
leftIcon: const FlowySvg(FlowySvgs.delete_s),
onTap: () {
if (enable) onDeleted?.call();
},
onHover: (_) => popoverMutex.close(),
);
return SizedBox(height: GridSize.popoverItemHeight, child: button);
},
);
}
}
@visibleForTesting
class FieldVisibilityToggleButton extends StatelessWidget {
final bool isFieldHidden;
final PopoverMutex popoverMutex;
final VoidCallback? onTap;
const FieldVisibilityToggleButton({
required this.isFieldHidden,
required this.popoverMutex,
required this.onTap,
super.key,
});
@override
Widget build(BuildContext context) {
return BlocBuilder<FieldEditorBloc, FieldEditorState>(
buildWhen: (previous, current) => previous != current,
builder: (context, state) {
final Widget button = FlowyButton(
text: FlowyText.medium(
isFieldHidden
? LocaleKeys.grid_field_show.tr()
: LocaleKeys.grid_field_hide.tr(),
),
leftIcon:
FlowySvg(isFieldHidden ? FlowySvgs.show_m : FlowySvgs.hide_m),
onTap: onTap,
onHover: (_) => popoverMutex.close(),
);
return SizedBox(height: GridSize.popoverItemHeight, child: button);
},
);
}
}

View File

@ -2,6 +2,7 @@ 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;
@ -10,7 +11,7 @@ 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 '../../layout/sizes.dart';
import 'field_type_extension.dart';
import 'field_type_list.dart';
import 'type_option/builder.dart';
@ -27,19 +28,17 @@ class FieldTypeOptionEditor extends StatelessWidget {
final PopoverMutex popoverMutex;
const FieldTypeOptionEditor({
super.key,
required TypeOptionController dataController,
required this.popoverMutex,
Key? key,
}) : _dataController = dataController,
super(key: key);
}) : _dataController = dataController;
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) {
final bloc = FieldTypeOptionEditBloc(_dataController);
bloc.add(const FieldTypeOptionEditEvent.initial());
return bloc;
return FieldTypeOptionEditBloc(_dataController)
..add(const FieldTypeOptionEditEvent.initial());
},
child: BlocBuilder<FieldTypeOptionEditBloc, FieldTypeOptionEditState>(
builder: (context, state) {
@ -53,8 +52,8 @@ class FieldTypeOptionEditor extends StatelessWidget {
if (typeOptionWidget != null) typeOptionWidget,
];
return ListView(
shrinkWrap: true,
return Column(
mainAxisSize: MainAxisSize.min,
children: children,
);
},
@ -74,22 +73,30 @@ class FieldTypeOptionEditor extends StatelessWidget {
}
}
class SwitchFieldButton extends StatelessWidget {
class SwitchFieldButton extends StatefulWidget {
final PopoverMutex popoverMutex;
const SwitchFieldButton({
super.key,
required this.popoverMutex,
Key? key,
}) : super(key: key);
});
@override
State<SwitchFieldButton> createState() => _SwitchFieldButtonState();
}
class _SwitchFieldButtonState extends State<SwitchFieldButton> {
final PopoverController _popoverController = PopoverController();
@override
Widget build(BuildContext context) {
final widget = AppFlowyPopover(
final child = AppFlowyPopover(
constraints: BoxConstraints.loose(const Size(460, 540)),
asBarrier: true,
triggerActions: PopoverTriggerFlags.click,
mutex: popoverMutex,
triggerActions: PopoverTriggerFlags.hover,
mutex: widget.popoverMutex,
controller: _popoverController,
offset: const Offset(8, 0),
popupBuilder: (popOverContext) {
margin: const EdgeInsets.all(8),
popupBuilder: (BuildContext popoverContext) {
return FieldTypeList(
onSelectField: (newFieldType) {
context
@ -99,20 +106,21 @@ class SwitchFieldButton extends StatelessWidget {
);
},
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 12.0),
padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: _buildMoreButton(context),
),
);
return SizedBox(
height: GridSize.popoverItemHeight,
child: widget,
child: child,
);
}
Widget _buildMoreButton(BuildContext context) {
final bloc = context.read<FieldTypeOptionEditBloc>();
return FlowyButton(
onTap: () => _popoverController.show(),
text: FlowyText.medium(
bloc.state.field.fieldType.title(),
),

View File

@ -1,7 +1,7 @@
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/type_option/type_option_context.dart';
import 'package:appflowy/plugins/database_view/grid/application/grid_bloc.dart';
import 'package:appflowy/plugins/database_view/grid/application/grid_header_bloc.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/mobile_field_cell.dart';
import 'package:appflowy_backend/log.dart';
@ -22,15 +22,13 @@ import 'field_cell.dart';
class GridHeaderSliverAdaptor extends StatefulWidget {
final String viewId;
final FieldController fieldController;
final ScrollController anchorScrollController;
const GridHeaderSliverAdaptor({
required this.viewId,
required this.fieldController,
required this.anchorScrollController,
Key? key,
}) : super(key: key);
super.key,
});
@override
State<GridHeaderSliverAdaptor> createState() =>
@ -40,11 +38,13 @@ class GridHeaderSliverAdaptor extends StatefulWidget {
class _GridHeaderSliverAdaptorState extends State<GridHeaderSliverAdaptor> {
@override
Widget build(BuildContext context) {
final fieldController =
context.read<GridBloc>().databaseController.fieldController;
return BlocProvider(
create: (context) {
return GridHeaderBloc(
viewId: widget.viewId,
fieldController: widget.fieldController,
fieldController: fieldController,
)..add(const GridHeaderEvent.initial());
},
child: BlocBuilder<GridHeaderBloc, GridHeaderState>(
@ -54,7 +54,10 @@ class _GridHeaderSliverAdaptorState extends State<GridHeaderSliverAdaptor> {
return SingleChildScrollView(
scrollDirection: Axis.horizontal,
controller: widget.anchorScrollController,
child: _GridHeader(viewId: widget.viewId),
child: _GridHeader(
viewId: widget.viewId,
fieldController: fieldController,
),
);
},
),
@ -64,7 +67,8 @@ class _GridHeaderSliverAdaptorState extends State<GridHeaderSliverAdaptor> {
class _GridHeader extends StatefulWidget {
final String viewId;
const _GridHeader({Key? key, required this.viewId}) : super(key: key);
final FieldController fieldController;
const _GridHeader({required this.viewId, required this.fieldController});
@override
State<_GridHeader> createState() => _GridHeaderState();
@ -98,6 +102,7 @@ class _GridHeaderState extends State<_GridHeader> {
key: _getKeyById(fieldInfo.id),
viewId: widget.viewId,
fieldInfo: fieldInfo,
fieldController: widget.fieldController,
)
: MobileFieldButton(
key: _getKeyById(fieldInfo.id),
@ -159,8 +164,9 @@ class _CellLeading extends StatelessWidget {
}
class _CellTrailing extends StatelessWidget {
const _CellTrailing({required this.viewId});
final String viewId;
const _CellTrailing({required this.viewId, Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
@ -179,7 +185,10 @@ class _CellTrailing extends StatelessWidget {
class CreateFieldButton extends StatefulWidget {
final String viewId;
const CreateFieldButton({required this.viewId, Key? key}) : super(key: key);
const CreateFieldButton({
super.key,
required this.viewId,
});
@override
State<CreateFieldButton> createState() => _CreateFieldButtonState();
@ -191,6 +200,8 @@ class _CreateFieldButtonState extends State<CreateFieldButton> {
@override
Widget build(BuildContext context) {
final fieldController =
context.read<GridBloc>().databaseController.fieldController;
return AppFlowyPopover(
controller: popoverController,
direction: PopoverDirection.bottomWithRightAligned,
@ -222,49 +233,13 @@ class _CreateFieldButtonState extends State<CreateFieldButton> {
},
leftIcon: const FlowySvg(FlowySvgs.add_s),
),
popupBuilder: (BuildContext popover) {
popupBuilder: (BuildContext popoverContext) {
return FieldEditor(
viewId: widget.viewId,
typeOptionLoader: FieldTypeOptionLoader(
viewId: widget.viewId,
field: typeOption.field_2,
),
fieldController: fieldController,
field: typeOption.field_2,
);
},
);
}
}
class SliverHeaderDelegateImplementation
extends SliverPersistentHeaderDelegate {
final String gridId;
final List<FieldPB> fields;
SliverHeaderDelegateImplementation({
required this.gridId,
required this.fields,
});
@override
Widget build(
BuildContext context,
double shrinkOffset,
bool overlapsContent,
) {
return _GridHeader(viewId: gridId);
}
@override
double get maxExtent => GridSize.headerHeight;
@override
double get minExtent => GridSize.headerHeight;
@override
bool shouldRebuild(covariant SliverPersistentHeaderDelegate oldDelegate) {
if (oldDelegate is SliverHeaderDelegateImplementation) {
return fields.length != oldDelegate.fields.length;
}
return true;
}
}

View File

@ -1,9 +1,13 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet_database_field_editor.dart';
import 'package:appflowy/plugins/database_view/application/field/field_info.dart';
import 'package:appflowy/plugins/database_view/grid/application/grid_bloc.dart';
import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'field_type_extension.dart';
@ -29,6 +33,8 @@ class MobileFieldButton extends StatelessWidget {
color: Theme.of(context).dividerColor,
width: 1.0,
);
final fieldController =
context.read<GridBloc>().databaseController.fieldController;
return Container(
width: field.fieldSettings!.width.toDouble(),
decoration: BoxDecoration(
@ -36,7 +42,14 @@ class MobileFieldButton extends StatelessWidget {
),
child: TextButton(
onLongPress: () {
debugPrint("gimme the bottom drawer");
showMobileBottomSheet(
context: context,
builder: (context) => MobileDBBottomSheetFieldEditor(
viewId: viewId,
field: field.field,
fieldController: fieldController,
),
);
},
style: TextButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 8),

View File

@ -42,11 +42,12 @@ class NumberTypeOptionWidgetBuilder extends TypeOptionWidgetBuilder {
class NumberTypeOptionWidget extends TypeOptionWidget {
final NumberTypeOptionContext typeOptionContext;
final PopoverMutex popoverMutex;
const NumberTypeOptionWidget({
super.key,
required this.typeOptionContext,
required this.popoverMutex,
Key? key,
}) : super(key: key);
});
@override
Widget build(BuildContext context) {
@ -60,7 +61,6 @@ class NumberTypeOptionWidget extends TypeOptionWidget {
final selectNumUnitButton = SizedBox(
height: GridSize.popoverItemHeight,
child: FlowyButton(
margin: GridSize.typeOptionContentInsets,
rightIcon: const FlowySvg(FlowySvgs.more_s),
text: FlowyText.regular(
state.typeOption.format.title(),
@ -72,13 +72,15 @@ class NumberTypeOptionWidget extends TypeOptionWidget {
padding: const EdgeInsets.only(left: 6),
height: GridSize.popoverItemHeight,
alignment: Alignment.centerLeft,
child: FlowyText.medium(
color: Theme.of(context).colorScheme.outline,
child: FlowyText.regular(
LocaleKeys.grid_field_numberFormat.tr(),
color: Theme.of(context).hintColor,
fontSize: 11,
),
);
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 12),
padding: const EdgeInsets.symmetric(horizontal: 8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@ -87,7 +89,7 @@ class NumberTypeOptionWidget extends TypeOptionWidget {
mutex: popoverMutex,
triggerActions:
PopoverTriggerFlags.hover | PopoverTriggerFlags.click,
offset: const Offset(8, 0),
offset: const Offset(16, 0),
constraints: BoxConstraints.loose(const Size(460, 440)),
margin: EdgeInsets.zero,
child: selectNumUnitButton,

View File

@ -3,7 +3,6 @@ import 'package:appflowy/plugins/database_view/application/field/type_option/sel
import 'package:appflowy_backend/protobuf/flowy-database2/select_option.pb.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
@ -40,23 +39,21 @@ class SelectOptionTypeOptionWidget extends StatelessWidget {
BlocBuilder<SelectOptionTypeOptionBloc, SelectOptionTypeOptionState>(
builder: (context, state) {
final List<Widget> children = [
const TypeOptionSeparator(),
const OptionTitle(),
if (state.isEditingOption)
const TypeOptionSeparator(spacing: 8),
const _OptionTitle(),
const VSpace(4),
if (state.isEditingOption) ...[
CreateOptionTextField(popoverMutex: popoverMutex),
if (state.options.isNotEmpty && state.isEditingOption)
const VSpace(10),
if (state.options.isEmpty && !state.isEditingOption)
const VSpace(4),
] else
const _AddOptionButton(),
const VSpace(4),
_OptionList(popoverMutex: popoverMutex),
];
return ListView.builder(
shrinkWrap: true,
itemCount: children.length,
itemBuilder: (context, index) {
return children[index];
},
return Column(
mainAxisSize: MainAxisSize.min,
children: children,
);
},
),
@ -64,31 +61,22 @@ class SelectOptionTypeOptionWidget extends StatelessWidget {
}
}
class OptionTitle extends StatelessWidget {
const OptionTitle({Key? key}) : super(key: key);
class _OptionTitle extends StatelessWidget {
const _OptionTitle();
@override
Widget build(BuildContext context) {
return BlocBuilder<SelectOptionTypeOptionBloc, SelectOptionTypeOptionState>(
builder: (context, state) {
final List<Widget> children = [
Padding(
padding: const EdgeInsets.only(left: 9),
child: FlowyText.medium(
LocaleKeys.grid_field_optionTitle.tr(),
),
),
];
if (state.options.isNotEmpty && !state.isEditingOption) {
children.add(const Spacer());
children.add(const _OptionTitleButton());
}
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 12.0),
child: SizedBox(
height: GridSize.popoverItemHeight,
child: Row(children: children),
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Align(
alignment: AlignmentDirectional.centerStart,
child: FlowyText.regular(
LocaleKeys.grid_field_optionTitle.tr(),
fontSize: 11,
color: Theme.of(context).hintColor,
),
),
);
},
@ -96,29 +84,6 @@ class OptionTitle extends StatelessWidget {
}
}
class _OptionTitleButton extends StatelessWidget {
const _OptionTitleButton({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return SizedBox(
width: 100,
height: 26,
child: FlowyButton(
text: FlowyText.medium(
LocaleKeys.grid_field_addOption.tr(),
textAlign: TextAlign.center,
),
onTap: () {
context
.read<SelectOptionTypeOptionBloc>()
.add(const SelectOptionTypeOptionEvent.addingOption());
},
),
);
}
}
class _OptionList extends StatelessWidget {
final PopoverMutex? popoverMutex;
const _OptionList({Key? key, this.popoverMutex}) : super(key: key);
@ -140,7 +105,6 @@ class _OptionList extends StatelessWidget {
return ListView.separated(
shrinkWrap: true,
controller: ScrollController(),
separatorBuilder: (context, index) {
return VSpace(GridSize.typeOptionSeparatorHeight);
},
@ -187,7 +151,7 @@ class _OptionCellState extends State<_OptionCell> {
@override
Widget build(BuildContext context) {
final child = SizedBox(
height: GridSize.popoverItemHeight,
height: 28,
child: SelectOptionTagCell(
option: widget.option,
onSelected: (SelectOptionPB pb) {
@ -212,7 +176,7 @@ class _OptionCellState extends State<_OptionCell> {
asBarrier: true,
constraints: BoxConstraints.loose(const Size(460, 470)),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 12.0),
padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: child,
),
popupBuilder: (BuildContext popoverContext) {
@ -243,24 +207,19 @@ class _AddOptionButton extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 12.0),
padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: SizedBox(
height: GridSize.popoverItemHeight,
child: FlowyButton(
hoverColor: AFThemeExtension.of(context).lightGreyHover,
text: FlowyText.medium(
LocaleKeys.grid_field_addSelectOption.tr(),
color: AFThemeExtension.of(context).textColor,
),
onTap: () {
context
.read<SelectOptionTypeOptionBloc>()
.add(const SelectOptionTypeOptionEvent.addingOption());
},
leftIcon: FlowySvg(
FlowySvgs.add_s,
color: Theme.of(context).iconTheme.color,
),
leftIcon: const FlowySvg(FlowySvgs.add_s),
),
),
);
@ -303,7 +262,7 @@ class _CreateOptionTextFieldState extends State<CreateOptionTextField> {
builder: (context, state) {
final text = state.newOptionName.foldRight("", (a, previous) => a);
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 12.0),
padding: const EdgeInsets.symmetric(horizontal: 14.0),
child: FlowyTextField(
autoClearWhenDone: true,
text: text,

View File

@ -62,20 +62,14 @@ class SelectOptionTypeOptionEditor extends StatelessWidget {
),
const VSpace(10),
const _DeleteTag(),
const TypeOptionSeparator(),
SelectOptionColorList(
selectedColor: state.option.color,
onSelectedColor: (color) => context
.read<EditSelectOptionBloc>()
.add(EditSelectOptionEvent.updateColor(color)),
),
];
if (showOptions) {
cells.add(const TypeOptionSeparator());
cells.add(
SelectOptionColorList(
selectedColor: state.option.color,
onSelectedColor: (color) => context
.read<EditSelectOptionBloc>()
.add(EditSelectOptionEvent.updateColor(color)),
),
);
}
return SizedBox(
width: 180,
child: ListView.builder(
@ -188,7 +182,6 @@ class SelectOptionColorList extends StatelessWidget {
),
ListView.separated(
shrinkWrap: true,
controller: ScrollController(),
separatorBuilder: (context, index) {
return VSpace(GridSize.typeOptionSeparatorHeight);
},

View File

@ -44,7 +44,6 @@ class SingleSelectTypeOptionWidget extends TypeOptionWidget {
},
popoverMutex: popoverMutex,
typeOptionAction: selectOptionAction,
// key: ValueKey(state.typeOption.hashCode),
);
}
}

View File

@ -91,7 +91,7 @@ class SelectOptionTag extends StatelessWidget {
@override
Widget build(BuildContext context) {
EdgeInsets padding =
const EdgeInsets.symmetric(vertical: 2, horizontal: 8.0);
const EdgeInsets.symmetric(vertical: 1, horizontal: 8.0);
if (onRemove != null) {
padding = padding.copyWith(right: 2.0);
}

View File

@ -109,7 +109,6 @@ class _OptionList extends StatelessWidget {
return ListView.separated(
shrinkWrap: true,
controller: ScrollController(),
itemCount: cells.length,
separatorBuilder: (_, __) =>
VSpace(GridSize.typeOptionSeparatorHeight),
@ -302,7 +301,7 @@ class _SelectOptionCellState extends State<_SelectOptionCell> {
@override
Widget build(BuildContext context) {
final child = SizedBox(
height: GridSize.popoverItemHeight,
height: 28,
child: SelectOptionTagCell(
option: widget.option,
onSelected: (option) {
@ -328,7 +327,6 @@ class _SelectOptionCellState extends State<_SelectOptionCell> {
FlowyIconButton(
onPressed: () => _popoverController.show(),
iconPadding: const EdgeInsets.symmetric(horizontal: 6.0),
// If hover color is none, it will use secondary color from the theme, we use [Colors.transparent] to remove the hover color
hoverColor: Colors.transparent,
icon: FlowySvg(
FlowySvgs.details_s,

View File

@ -1,3 +1,4 @@
import 'package:appflowy/plugins/database_view/application/field/field_controller.dart';
import 'package:appflowy/plugins/database_view/application/row/row_controller.dart';
import 'package:appflowy/plugins/database_view/grid/application/row/row_detail_bloc.dart';
import 'package:appflowy/plugins/database_view/widgets/row/row_document.dart';
@ -11,11 +12,13 @@ import 'row_banner.dart';
import 'row_property.dart';
class RowDetailPage extends StatefulWidget with FlowyOverlayDelegate {
final FieldController fieldController;
final RowController rowController;
final GridCellBuilder cellBuilder;
const RowDetailPage({
super.key,
required this.fieldController,
required this.rowController,
required this.cellBuilder,
});
@ -56,6 +59,7 @@ class _RowDetailPageState extends State<RowDetailPage> {
child: RowPropertyList(
cellBuilder: widget.cellBuilder,
viewId: widget.rowController.viewId,
fieldController: widget.fieldController,
),
),
const VSpace(20),

View File

@ -3,14 +3,13 @@ import 'dart:io';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/database_view/application/cell/cell_service.dart';
import 'package:appflowy/plugins/database_view/application/field/type_option/type_option_context.dart';
import 'package:appflowy/plugins/database_view/application/field/field_controller.dart';
import 'package:appflowy/plugins/database_view/application/field/type_option/type_option_service.dart';
import 'package:appflowy/plugins/database_view/grid/application/row/row_detail_bloc.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/field_cell.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/field_editor.dart';
import 'package:appflowy/plugins/database_view/widgets/row/cells/cells.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/block_action_button.dart';
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
@ -28,12 +27,15 @@ import 'cell_builder.dart';
/// [RowDetailPage].
class RowPropertyList extends StatelessWidget {
final String viewId;
final FieldController fieldController;
final GridCellBuilder cellBuilder;
const RowPropertyList({
super.key,
required this.viewId,
required this.fieldController,
required this.cellBuilder,
Key? key,
}) : super(key: key);
});
@override
Widget build(BuildContext context) {
@ -46,6 +48,7 @@ class RowPropertyList extends StatelessWidget {
key: ValueKey('row_detail_${cell.fieldId}'),
cellContext: cell,
cellBuilder: cellBuilder,
fieldController: fieldController,
index: index,
),
)
@ -99,7 +102,10 @@ class RowPropertyList extends StatelessWidget {
padding: EdgeInsets.only(bottom: 4.0),
child: ToggleHiddenFieldsVisibilityButton(),
),
CreateRowFieldButton(viewId: viewId),
CreateRowFieldButton(
viewId: viewId,
fieldController: fieldController,
),
],
),
),
@ -113,14 +119,16 @@ class RowPropertyList extends StatelessWidget {
class _PropertyCell extends StatefulWidget {
final DatabaseCellContext cellContext;
final GridCellBuilder cellBuilder;
final FieldController fieldController;
final int index;
const _PropertyCell({
super.key,
required this.cellContext,
required this.cellBuilder,
Key? key,
required this.fieldController,
required this.index,
}) : super(key: key);
});
@override
State<StatefulWidget> createState() => _PropertyCellState();
@ -219,30 +227,8 @@ class _PropertyCellState extends State<_PropertyCell> {
Widget buildFieldEditor() {
return FieldEditor(
viewId: widget.cellContext.viewId,
fieldInfo: widget.cellContext.fieldInfo,
isGroupingField: widget.cellContext.fieldInfo.isGroupField,
typeOptionLoader: FieldTypeOptionLoader(
viewId: widget.cellContext.viewId,
field: widget.cellContext.fieldInfo.field,
),
onToggleVisibility: (fieldId) {
_popoverController.close();
context
.read<RowDetailBloc>()
.add(RowDetailEvent.toggleFieldVisibility(fieldId));
},
onDeleted: (fieldId) {
_popoverController.close();
NavigatorAlertDialog(
title: LocaleKeys.grid_field_deleteFieldPromptMessage.tr(),
confirm: () {
context
.read<RowDetailBloc>()
.add(RowDetailEvent.deleteField(fieldId));
},
).show(context);
},
field: widget.cellContext.fieldInfo.field,
fieldController: widget.fieldController,
);
}
}
@ -342,8 +328,13 @@ class ToggleHiddenFieldsVisibilityButton extends StatelessWidget {
class CreateRowFieldButton extends StatefulWidget {
final String viewId;
final FieldController fieldController;
const CreateRowFieldButton({required this.viewId, super.key});
const CreateRowFieldButton({
super.key,
required this.viewId,
required this.fieldController,
});
@override
State<CreateRowFieldButton> createState() => _CreateRowFieldButtonState();
@ -394,24 +385,11 @@ class _CreateRowFieldButtonState extends State<CreateRowFieldButton> {
),
),
),
popupBuilder: (BuildContext popOverContext) {
popupBuilder: (BuildContext popoverContext) {
return FieldEditor(
viewId: widget.viewId,
typeOptionLoader: FieldTypeOptionLoader(
viewId: widget.viewId,
field: typeOption.field_2,
),
onDeleted: (fieldId) {
popoverController.close();
NavigatorAlertDialog(
title: LocaleKeys.grid_field_deleteFieldPromptMessage.tr(),
confirm: () {
context
.read<RowDetailBloc>()
.add(RowDetailEvent.deleteField(fieldId));
},
).show(context);
},
field: typeOption.field_2,
fieldController: widget.fieldController,
);
},
);

View File

@ -3,22 +3,19 @@ import 'dart:io';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/plugins/database_view/application/field/field_controller.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/setting/property_bloc.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/layout/sizes.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/field_editor.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/field_type_extension.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:collection/collection.dart';
import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:styled_widget/styled_widget.dart';
import '../../grid/presentation/layout/sizes.dart';
import '../../grid/presentation/widgets/header/field_editor.dart';
class DatabasePropertyList extends StatefulWidget {
final String viewId;
final FieldController fieldController;
@ -49,6 +46,7 @@ class _DatabasePropertyListState extends State<DatabasePropertyList> {
return DatabasePropertyCell(
key: ValueKey(field.id),
viewId: widget.viewId,
fieldController: widget.fieldController,
fieldInfo: field,
popoverMutex: _popoverMutex,
index: index,
@ -93,6 +91,7 @@ class _DatabasePropertyListState extends State<DatabasePropertyList> {
@visibleForTesting
class DatabasePropertyCell extends StatefulWidget {
final FieldController fieldController;
final FieldInfo fieldInfo;
final String viewId;
final PopoverMutex popoverMutex;
@ -104,6 +103,7 @@ class DatabasePropertyCell extends StatefulWidget {
required this.viewId,
required this.popoverMutex,
required this.index,
required this.fieldController,
});
@override
@ -120,6 +120,7 @@ class _DatabasePropertyCellState extends State<DatabasePropertyCell> {
visiblity != null && visiblity != FieldVisibility.AlwaysHidden
? FlowySvgs.show_m
: FlowySvgs.hide_m,
size: const Size.square(16),
color: Theme.of(context).iconTheme.color,
);
@ -182,7 +183,7 @@ class _DatabasePropertyCellState extends State<DatabasePropertyCell> {
),
);
},
icon: visibleIcon.padding(all: 4.0),
icon: visibleIcon,
),
onTap: () => _popoverController.show(),
).padding(horizontal: 6.0),
@ -190,11 +191,8 @@ class _DatabasePropertyCellState extends State<DatabasePropertyCell> {
popupBuilder: (BuildContext context) {
return FieldEditor(
viewId: widget.viewId,
fieldInfo: widget.fieldInfo,
typeOptionLoader: FieldTypeOptionLoader(
viewId: widget.viewId,
field: widget.fieldInfo.field,
),
field: widget.fieldInfo.field,
fieldController: widget.fieldController,
);
},
);

View File

@ -454,10 +454,12 @@ GoRoute _mobileCardDetailScreenRoute() {
pageBuilder: (context, state) {
final args = state.extra as Map<String, dynamic>;
final rowController = args[MobileCardDetailScreen.argRowController];
final fieldController = args[MobileCardDetailScreen.argFieldController];
return MaterialPage(
child: MobileCardDetailScreen(
rowController: rowController,
fieldController: fieldController,
),
);
},
@ -471,6 +473,7 @@ GoRoute _mobileCardPropertyEditScreenRoute() {
pageBuilder: (context, state) {
final args = state.extra as Map<String, dynamic>;
final cellContext = args[CardPropertyEditScreen.argCellContext];
final fieldController = args[CardPropertyEditScreen.argFieldController];
final rowDetailBloc = args[CardPropertyEditScreen.argRowDetailBloc];
return MaterialPage(
@ -478,6 +481,7 @@ GoRoute _mobileCardPropertyEditScreenRoute() {
value: rowDetailBloc as RowDetailBloc,
child: CardPropertyEditScreen(
cellContext: cellContext,
fieldController: fieldController,
),
),
);
@ -508,11 +512,16 @@ GoRoute _mobileCreateRowFieldScreenRoute() {
pageBuilder: (context, state) {
final args = state.extra as Map<String, dynamic>;
final viewId = args[MobileCreateRowFieldScreen.argViewId];
final fieldController =
args[MobileCreateRowFieldScreen.argFieldController];
final typeOption = args[MobileCreateRowFieldScreen.argTypeOption];
return MaterialPage(
child:
MobileCreateRowFieldScreen(viewId: viewId, typeOption: typeOption),
child: MobileCreateRowFieldScreen(
viewId: viewId,
typeOption: typeOption,
fieldController: fieldController,
),
fullscreenDialog: true,
);
},

View File

@ -185,7 +185,7 @@ class FlowyTextButton extends StatelessWidget {
List<Widget> children = [];
if (heading != null) {
children.add(heading!);
children.add(const HSpace(6));
children.add(const HSpace(8));
}
children.add(
FlowyText(

View File

@ -40,13 +40,14 @@ void main() {
);
final editorBloc = FieldEditorBloc(
isGroupField: fieldInfo.isGroupField,
viewId: context.gridView.id,
loader: loader,
field: fieldInfo.field,
fieldController: context.fieldController,
)..add(const FieldEditorEvent.initial());
await boardResponseFuture();
editorBloc.add(const FieldEditorEvent.updateName('Hello world'));
editorBloc.add(const FieldEditorEvent.renameField('Hello world'));
await boardResponseFuture();
// assert the groups were not changed

View File

@ -29,13 +29,10 @@ void main() {
build: () => editorBloc,
wait: boardResponseDuration(),
act: (bloc) async {
bloc.add(const FieldEditorEvent.switchToField(FieldType.RichText));
bloc.add(const FieldEditorEvent.switchFieldType(FieldType.RichText));
},
verify: (bloc) {
bloc.state.field.fold(
() => throw Exception(),
(field) => field.fieldType == FieldType.RichText,
);
assert(bloc.state.field.fieldType == FieldType.RichText);
},
);
blocTest<BoardBloc, BoardState>(

View File

@ -88,7 +88,8 @@ class BoardTestContext {
);
final editorBloc = FieldEditorBloc(
isGroupField: fieldInfo.isGroupField,
viewId: databaseController.viewId,
fieldController: fieldController,
loader: loader,
field: fieldInfo.field,
);
@ -126,10 +127,11 @@ class BoardTestContext {
}
Future<FieldEditorBloc> createField(FieldType fieldType) async {
final editorBloc = await createFieldEditor(viewId: gridView.id)
..add(const FieldEditorEvent.initial());
final editorBloc =
await createFieldEditor(databaseController: _boardDataController)
..add(const FieldEditorEvent.initial());
await gridResponseFuture();
editorBloc.add(FieldEditorEvent.switchToField(fieldType));
editorBloc.add(FieldEditorEvent.switchFieldType(fieldType));
await gridResponseFuture();
return Future(() => editorBloc);
}

View File

@ -13,7 +13,8 @@ Future<FieldEditorBloc> createEditorBloc(AppFlowyGridTest gridTest) async {
);
return FieldEditorBloc(
isGroupField: fieldInfo.isGroupField,
viewId: context.gridView.id,
fieldController: context.fieldController,
loader: loader,
field: fieldInfo.field,
)..add(const FieldEditorEvent.initial());
@ -27,67 +28,21 @@ void main() {
});
test('rename field', () async {
final editorBloc = await makeEditorBloc(gridTest);
editorBloc.add(const FieldEditorEvent.updateName('Hello world'));
await gridResponseFuture();
final editorBloc = await createEditorBloc(gridTest);
editorBloc.add(const FieldEditorEvent.renameField('Hello world'));
editorBloc.state.field.fold(
() => throw Exception("The field should not be none"),
(field) {
assert(field.name == 'Hello world');
},
);
await gridResponseFuture();
expect(editorBloc.state.field.name, equals("Hello world"));
});
test('switch to text field', () async {
final editorBloc = await makeEditorBloc(gridTest);
final editorBloc = await createEditorBloc(gridTest);
editorBloc.add(const FieldEditorEvent.switchToField(FieldType.RichText));
editorBloc.add(const FieldEditorEvent.switchFieldType(FieldType.RichText));
await gridResponseFuture();
editorBloc.state.field.fold(
() => throw Exception("The field should not be none"),
(field) {
// The default length of the fields is 3. The length of the fields
// should not change after switching to other field type
// assert(gridTest.fieldContexts.length == 3);
assert(field.fieldType == FieldType.RichText);
},
);
});
test('delete field', () async {
final editorBloc = await makeEditorBloc(gridTest);
editorBloc.add(const FieldEditorEvent.switchToField(FieldType.RichText));
await gridResponseFuture();
editorBloc.state.field.fold(
() => throw Exception("The field should not be none"),
(field) {
// The default length of the fields is 3. The length of the fields
// should not change after switching to other field type
// assert(gridTest.fieldContexts.length == 3);
assert(field.fieldType == FieldType.RichText);
},
);
// The default length of the fields is 3. The length of the fields
// should not change after switching to other field type
expect(editorBloc.state.field.fieldType, equals(FieldType.RichText));
});
}
Future<FieldEditorBloc> makeEditorBloc(AppFlowyGridTest gridTest) async {
final context = await gridTest.createTestGrid();
final fieldInfo = context.singleSelectFieldContext();
final loader = FieldTypeOptionLoader(
viewId: context.gridView.id,
field: fieldInfo.field,
);
final editorBloc = FieldEditorBloc(
isGroupField: fieldInfo.isGroupField,
loader: loader,
field: fieldInfo.field,
)..add(const FieldEditorEvent.initial());
await gridResponseFuture();
return editorBloc;
}

View File

@ -1,128 +0,0 @@
import 'package:appflowy/plugins/database_view/application/field/field_action_sheet_bloc.dart';
import 'package:appflowy/plugins/database_view/grid/application/grid_header_bloc.dart';
import 'package:bloc_test/bloc_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'util.dart';
void main() {
late AppFlowyGridTest gridTest;
setUpAll(() async {
gridTest = await AppFlowyGridTest.ensureInitialized();
});
group('$GridHeaderBloc', () {
late FieldActionSheetBloc actionSheetBloc;
late GridTestContext context;
setUp(() async {
context = await gridTest.createTestGrid();
actionSheetBloc = FieldActionSheetBloc(
viewId: context.gridView.id,
fieldInfo: context.singleSelectFieldContext(),
);
});
blocTest<GridHeaderBloc, GridHeaderState>(
"hides property",
build: () {
final bloc = GridHeaderBloc(
viewId: context.gridView.id,
fieldController: context.fieldController,
)..add(const GridHeaderEvent.initial());
return bloc;
},
act: (bloc) async {
actionSheetBloc.add(const FieldActionSheetEvent.hideField());
await Future.delayed(gridResponseDuration());
},
wait: gridResponseDuration(),
verify: (bloc) {
assert(bloc.state.fields.length == 2);
},
);
blocTest<GridHeaderBloc, GridHeaderState>(
"shows property",
build: () {
final bloc = GridHeaderBloc(
viewId: context.gridView.id,
fieldController: context.fieldController,
)..add(const GridHeaderEvent.initial());
return bloc;
},
act: (bloc) async {
actionSheetBloc.add(const FieldActionSheetEvent.hideField());
await Future.delayed(gridResponseDuration());
actionSheetBloc.add(const FieldActionSheetEvent.showField());
await Future.delayed(gridResponseDuration());
},
wait: gridResponseDuration(),
verify: (bloc) {
assert(bloc.state.fields.length == 3);
},
);
blocTest<GridHeaderBloc, GridHeaderState>(
"duplicate property",
build: () {
final bloc = GridHeaderBloc(
viewId: context.gridView.id,
fieldController: context.fieldController,
)..add(const GridHeaderEvent.initial());
return bloc;
},
act: (bloc) async {
actionSheetBloc.add(const FieldActionSheetEvent.duplicateField());
await Future.delayed(gridResponseDuration());
},
wait: gridResponseDuration(),
verify: (bloc) {
expect(bloc.state.fields.length, 4);
},
);
blocTest<GridHeaderBloc, GridHeaderState>(
"delete property",
build: () {
final bloc = GridHeaderBloc(
viewId: context.gridView.id,
fieldController: context.fieldController,
)..add(const GridHeaderEvent.initial());
return bloc;
},
act: (bloc) async {
actionSheetBloc.add(const FieldActionSheetEvent.deleteField());
await Future.delayed(gridResponseDuration());
},
wait: gridResponseDuration(),
verify: (bloc) {
expect(bloc.state.fields.length, 2);
},
);
blocTest<GridHeaderBloc, GridHeaderState>(
"update name",
build: () {
final bloc = GridHeaderBloc(
viewId: context.gridView.id,
fieldController: context.fieldController,
)..add(const GridHeaderEvent.initial());
return bloc;
},
act: (bloc) async {
actionSheetBloc
.add(const FieldActionSheetEvent.updateFieldName("Hello world"));
await Future.delayed(gridResponseDuration());
},
wait: gridResponseDuration(),
verify: (bloc) {
final field = bloc.state.fields.firstWhere(
(element) => element.id == actionSheetBloc.fieldService.fieldId,
);
expect(field.name, "Hello world");
},
);
});
}

View File

@ -73,10 +73,11 @@ class GridTestContext {
}
Future<FieldEditorBloc> createField(FieldType fieldType) async {
final editorBloc = await createFieldEditor(viewId: gridView.id)
..add(const FieldEditorEvent.initial());
final editorBloc =
await createFieldEditor(databaseController: gridController)
..add(const FieldEditorEvent.initial());
await gridResponseFuture();
editorBloc.add(FieldEditorEvent.switchToField(fieldType));
editorBloc.add(FieldEditorEvent.switchFieldType(fieldType));
await gridResponseFuture();
return Future(() => editorBloc);
}
@ -132,19 +133,21 @@ class GridTestContext {
}
Future<FieldEditorBloc> createFieldEditor({
required String viewId,
required DatabaseController databaseController,
}) async {
final result = await TypeOptionBackendService.createFieldTypeOption(
viewId: viewId,
viewId: databaseController.viewId,
);
await gridResponseFuture();
return result.fold(
(data) {
final loader = FieldTypeOptionLoader(
viewId: viewId,
viewId: databaseController.viewId,
field: data.field_2,
);
return FieldEditorBloc(
isGroupField: FieldInfo.initial(data.field_2).isGroupField,
viewId: databaseController.viewId,
fieldController: databaseController.fieldController,
loader: loader,
field: data.field_2,
);