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(); 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 // multiple text cell
testWidgets('edit multiple text cells', (tester) async { testWidgets('edit multiple text cells', (tester) async {
await tester.initializeAppFlowy(); await tester.initializeAppFlowy();

View File

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

View File

@ -123,7 +123,6 @@ void main() {
await tester.tapTypeOptionButton(); await tester.tapTypeOptionButton();
await tester.selectFieldType(fieldType); await tester.selectFieldType(fieldType);
await tester.dismissFieldEditor();
// After update the field type, the cells should be updated // After update the field type, the cells should be updated
await tester.findCellByFieldType(fieldType); 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/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/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.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_editor.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/field_type_extension.dart'; import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/field_type_extension.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/field_type_list.dart'; import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/field_type_list.dart';
@ -688,7 +687,10 @@ extension AppFlowyDatabaseTest on WidgetTester {
} }
Future<void> tapDeletePropertyInFieldEditor() async { Future<void> tapDeletePropertyInFieldEditor() async {
final deleteButton = find.byType(DeleteFieldButton); final deleteButton = find.byWidgetPredicate(
(widget) =>
widget is FieldActionCell && widget.action == FieldAction.delete,
);
await tapButton(deleteButton); await tapButton(deleteButton);
final confirmButton = find.descendant( final confirmButton = find.descendant(
@ -765,13 +767,18 @@ extension AppFlowyDatabaseTest on WidgetTester {
Future<void> tapHidePropertyButton() async { Future<void> tapHidePropertyButton() async {
final field = find.byWidgetPredicate( final field = find.byWidgetPredicate(
(widget) => (widget) =>
widget is FieldActionCell && widget.action == FieldAction.hide, widget is FieldActionCell &&
widget.action == FieldAction.toggleVisibility,
); );
await tapButton(field); await tapButton(field);
} }
Future<void> tapHidePropertyButtonInFieldEditor() async { Future<void> tapHidePropertyButtonInFieldEditor() async {
final button = find.byType(FieldVisibilityToggleButton); final button = find.byWidgetPredicate(
(widget) =>
widget is FieldActionCell &&
widget.action == FieldAction.toggleVisibility,
);
await tapButton(button); 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, super.key,
required this.name, required this.name,
required this.onRename, required this.onRename,
this.padding = const EdgeInsets.symmetric(horizontal: 12.0, vertical: 16.0),
}); });
final String name; final String name;
final void Function(String name) onRename; final void Function(String name) onRename;
final EdgeInsets padding;
@override @override
State<MobileBottomSheetRenameWidget> createState() => State<MobileBottomSheetRenameWidget> createState() =>
@ -39,17 +41,13 @@ class _MobileBottomSheetRenameWidgetState
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Padding( return Padding(
padding: const EdgeInsets.symmetric( padding: widget.padding,
horizontal: 4.0,
vertical: 16.0,
),
child: Row( child: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
const HSpace(8.0),
Expanded( Expanded(
child: SizedBox( child: SizedBox(
height: 44.0, height: 42.0,
child: FlowyTextField( child: FlowyTextField(
controller: controller, controller: controller,
), ),
@ -58,17 +56,16 @@ class _MobileBottomSheetRenameWidgetState
const HSpace(12.0), const HSpace(12.0),
FlowyTextButton( FlowyTextButton(
LocaleKeys.button_edit.tr(), LocaleKeys.button_edit.tr(),
constraints: const BoxConstraints.tightFor(height: 42),
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
vertical: 12.0,
horizontal: 16.0, horizontal: 16.0,
), ),
fontColor: Colors.white, fontColor: Colors.white,
fillColor: Colors.lightBlue.shade300, fillColor: Theme.of(context).primaryColor,
onPressed: () { onPressed: () {
widget.onRename(controller.text); 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/bottom_sheet/bottom_sheet_action_widget.dart';
import 'package:appflowy/mobile/presentation/widgets/show_flowy_mobile_bottom_sheet.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/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/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_banner_bloc.dart';
import 'package:appflowy/plugins/database_view/application/row/row_controller.dart'; import 'package:appflowy/plugins/database_view/application/row/row_controller.dart';
@ -24,12 +25,16 @@ class MobileCardDetailScreen extends StatefulWidget {
const MobileCardDetailScreen({ const MobileCardDetailScreen({
super.key, super.key,
required this.rowController, required this.rowController,
required this.fieldController,
}); });
static const routeName = '/MobileCardDetailScreen'; static const routeName = '/MobileCardDetailScreen';
static const argRowController = 'rowController'; static const argRowController = 'rowController';
static const argCellBuilder = 'cellBuilder'; static const argCellBuilder = 'cellBuilder';
static const argFieldController = 'fieldController';
final RowController rowController; final RowController rowController;
final FieldController fieldController;
@override @override
State<MobileCardDetailScreen> createState() => _MobileCardDetailScreenState(); State<MobileCardDetailScreen> createState() => _MobileCardDetailScreenState();
@ -169,6 +174,7 @@ class _MobileCardDetailScreenState extends State<MobileCardDetailScreen> {
MobileRowPropertyList( MobileRowPropertyList(
cellBuilder: _cellBuilder, cellBuilder: _cellBuilder,
viewId: widget.rowController.viewId, viewId: widget.rowController.viewId,
fieldController: widget.fieldController,
), ),
const Divider(), const Divider(),
const VSpace(16), const VSpace(16),

View File

@ -1,5 +1,6 @@
import 'package:appflowy/generated/locale_keys.g.dart'; 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/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/plugins/database_view/application/field/type_option/type_option_context.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
@ -9,15 +10,18 @@ import 'package:go_router/go_router.dart';
class MobileCreateRowFieldScreen extends StatefulWidget { class MobileCreateRowFieldScreen extends StatefulWidget {
static const routeName = '/MobileCreateRowFieldScreen'; static const routeName = '/MobileCreateRowFieldScreen';
static const argViewId = 'viewId'; static const argViewId = 'viewId';
static const argFieldController = 'fieldController';
static const argTypeOption = 'typeOption'; static const argTypeOption = 'typeOption';
const MobileCreateRowFieldScreen({ const MobileCreateRowFieldScreen({
super.key,
required this.viewId, required this.viewId,
required this.typeOption, required this.typeOption,
super.key, required this.fieldController,
}); });
final String viewId; final String viewId;
final FieldController fieldController;
final TypeOptionPB typeOption; final TypeOptionPB typeOption;
@override @override
@ -53,6 +57,8 @@ class _MobileCreateRowFieldScreenState
viewId: widget.viewId, viewId: widget.viewId,
field: widget.typeOption.field_2, 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/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.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/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/plugins/database_view/application/field/type_option/type_option_service.dart';
import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/log.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
@ -8,9 +9,14 @@ import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
class MobileCreateRowFieldButton extends StatelessWidget { 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 String viewId;
final FieldController fieldController;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -32,6 +38,7 @@ class MobileCreateRowFieldButton extends StatelessWidget {
extra: { extra: {
MobileCreateRowFieldScreen.argViewId: viewId, MobileCreateRowFieldScreen.argViewId: viewId,
MobileCreateRowFieldScreen.argTypeOption: typeOption, MobileCreateRowFieldScreen.argTypeOption: typeOption,
MobileCreateRowFieldScreen.argFieldController: fieldController,
}, },
); );
}, },

View File

@ -38,7 +38,7 @@ class _MobileFieldNameTextFieldState extends State<MobileFieldNameTextField> {
onChanged: (newName) { onChanged: (newName) {
context context
.read<FieldEditorBloc>() .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_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/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/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/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/grid/presentation/widgets/header/field_type_extension.dart';
import 'package:appflowy/plugins/database_view/widgets/row/accessory/cell_accessory.dart'; import 'package:appflowy/plugins/database_view/widgets/row/accessory/cell_accessory.dart';
@ -23,10 +24,12 @@ class MobileRowPropertyList extends StatelessWidget {
const MobileRowPropertyList({ const MobileRowPropertyList({
super.key, super.key,
required this.viewId, required this.viewId,
required this.fieldController,
required this.cellBuilder, required this.cellBuilder,
}); });
final String viewId; final String viewId;
final FieldController fieldController;
final GridCellBuilder cellBuilder; final GridCellBuilder cellBuilder;
@override @override
@ -44,6 +47,7 @@ class MobileRowPropertyList extends StatelessWidget {
itemBuilder: (context, index) => _PropertyCell( itemBuilder: (context, index) => _PropertyCell(
key: ValueKey('row_detail_${visibleCells[index].fieldId}'), key: ValueKey('row_detail_${visibleCells[index].fieldId}'),
cellContext: visibleCells[index], cellContext: visibleCells[index],
fieldController: fieldController,
cellBuilder: cellBuilder, cellBuilder: cellBuilder,
index: index, index: index,
), ),
@ -75,6 +79,7 @@ class MobileRowPropertyList extends StatelessWidget {
// add new field // add new field
MobileCreateRowFieldButton( MobileCreateRowFieldButton(
viewId: viewId, viewId: viewId,
fieldController: fieldController,
), ),
], ],
), ),
@ -93,11 +98,13 @@ class _PropertyCell extends StatefulWidget {
const _PropertyCell({ const _PropertyCell({
super.key, super.key,
required this.cellContext, required this.cellContext,
required this.fieldController,
required this.cellBuilder, required this.cellBuilder,
required this.index, required this.index,
}); });
final DatabaseCellContext cellContext; final DatabaseCellContext cellContext;
final FieldController fieldController;
final GridCellBuilder cellBuilder; final GridCellBuilder cellBuilder;
final int index; final int index;
@ -142,6 +149,7 @@ class _PropertyCellState extends State<_PropertyCell> {
CardPropertyEditScreen.routeName, CardPropertyEditScreen.routeName,
extra: { extra: {
CardPropertyEditScreen.argCellContext: widget.cellContext, CardPropertyEditScreen.argCellContext: widget.cellContext,
CardPropertyEditScreen.argFieldController: widget.fieldController,
CardPropertyEditScreen.argRowDetailBloc: CardPropertyEditScreen.argRowDetailBloc:
context.read<RowDetailBloc>(), 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/database/card/card_property_edit/mobile_field_editor.dart';
import 'package:appflowy/mobile/presentation/widgets/show_flowy_mobile_confirm_dialog.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/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/application/field/type_option/type_option_context.dart';
import 'package:appflowy/plugins/database_view/grid/application/row/row_detail_bloc.dart'; import 'package:appflowy/plugins/database_view/grid/application/row/row_detail_bloc.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
@ -14,13 +15,16 @@ class CardPropertyEditScreen extends StatelessWidget {
const CardPropertyEditScreen({ const CardPropertyEditScreen({
super.key, super.key,
required this.cellContext, required this.cellContext,
required this.fieldController,
}); });
static const routeName = '/CardPropertyEditScreen'; static const routeName = '/CardPropertyEditScreen';
static const argCellContext = 'cellContext'; static const argCellContext = 'cellContext';
static const argFieldController = 'fieldController';
static const argRowDetailBloc = 'rowDetailBloc'; static const argRowDetailBloc = 'rowDetailBloc';
final DatabaseCellContext cellContext; final DatabaseCellContext cellContext;
final FieldController fieldController;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -57,7 +61,8 @@ class CardPropertyEditScreen extends StatelessWidget {
viewId: cellContext.viewId, viewId: cellContext.viewId,
field: cellContext.fieldInfo.field, 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_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/mobile_field_type_option_editor.dart';
import 'package:appflowy/mobile/presentation/database/card/card_property_edit/widgets/property_title.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_editor_bloc.dart';
import 'package:appflowy/plugins/database_view/application/field/field_info.dart';
import 'package:appflowy/plugins/database_view/application/field/type_option/type_option_context.dart'; import 'package:appflowy/plugins/database_view/application/field/type_option/type_option_context.dart';
import 'package:appflowy/plugins/database_view/grid/application/row/row_detail_bloc.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/protobuf.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/field_settings_entities.pbenum.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/widget/spacing.dart'; import 'package:flowy_infra_ui/widget/spacing.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -19,65 +18,63 @@ class MobileFieldEditor extends StatelessWidget {
super.key, super.key,
required this.viewId, required this.viewId,
required this.typeOptionLoader, required this.typeOptionLoader,
this.isGroupingField = false, required this.field,
this.fieldInfo, required this.fieldController,
}); });
final String viewId; final String viewId;
final bool isGroupingField; final FieldController fieldController;
final FieldTypeOptionLoader typeOptionLoader; final FieldTypeOptionLoader typeOptionLoader;
final FieldInfo? fieldInfo; final FieldPB field;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return BlocProvider( return BlocProvider(
create: (context) { create: (context) {
return FieldEditorBloc( return FieldEditorBloc(
// group field is the field to be used to group cards in database view, it can not be deleted viewId: viewId,
isGroupField: isGroupingField,
loader: typeOptionLoader, loader: typeOptionLoader,
field: typeOptionLoader.field, field: field,
fieldController: fieldController,
)..add(const FieldEditorEvent.initial()); )..add(const FieldEditorEvent.initial());
}, },
child: BlocBuilder<FieldEditorBloc, FieldEditorState>( child: BlocBuilder<FieldEditorBloc, FieldEditorState>(
builder: (context, state) { builder: (context, state) {
// for field type edit option // for field type edit option
final dataController = context.read<FieldEditorBloc>().dataController; final dataController =
context.read<FieldEditorBloc>().typeOptionController;
return Padding( return Padding(
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
// property name // field name
// TODO(yijing): improve hint text // TODO(yijing): improve hint text
PropertyTitle(LocaleKeys.settings_user_name.tr()), PropertyTitle(LocaleKeys.settings_user_name.tr()),
BlocSelector<FieldEditorBloc, FieldEditorState, String>( BlocSelector<FieldEditorBloc, FieldEditorState, String>(
selector: (state) { selector: (state) => state.field.name,
return state.name; builder: (context, fieldName) {
},
builder: (context, propertyName) {
return MobileFieldNameTextField( return MobileFieldNameTextField(
text: propertyName, text: fieldName,
); );
}, },
), ),
Row( Row(
children: [ children: [
PropertyTitle(LocaleKeys.grid_field_visibility.tr()), Expanded(
const Spacer(), child:
PropertyTitle(LocaleKeys.grid_field_visibility.tr()),
),
VisibilitySwitch( VisibilitySwitch(
isFieldHidden: isFieldHidden: state.field.visibility ==
fieldInfo?.visibility == FieldVisibility.AlwaysHidden, FieldVisibility.AlwaysHidden,
onChanged: () { onChanged: () {
state.field.fold( context.read<RowDetailBloc>().add(
() => Log.error('Can not hidden the field'), RowDetailEvent.toggleFieldVisibility(
(field) => context.read<RowDetailBloc>().add( state.field.id,
RowDetailEvent.toggleFieldVisibility(
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_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:flutter_bloc/flutter_bloc.dart';
import 'package:dartz/dartz.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 'field_service.dart';
import 'type_option/type_option_context.dart'; import 'type_option/type_option_context.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'type_option/type_option_data_controller.dart'; import 'type_option/type_option_data_controller.dart';
part 'field_editor_bloc.freezed.dart'; part 'field_editor_bloc.freezed.dart';
class FieldEditorBloc extends Bloc<FieldEditorEvent, FieldEditorState> { class FieldEditorBloc extends Bloc<FieldEditorEvent, FieldEditorState> {
final TypeOptionController dataController; final FieldPB field;
final String viewId;
final FieldController fieldController;
final SingleFieldListener _singleFieldListener;
final FieldBackendService fieldService;
final FieldSettingsBackendService fieldSettingsService;
final TypeOptionController typeOptionController;
FieldEditorBloc({ FieldEditorBloc({
required bool isGroupField, required this.viewId,
required FieldPB field, required this.field,
required this.fieldController,
required FieldTypeOptionLoader loader, required FieldTypeOptionLoader loader,
}) : dataController = TypeOptionController( }) : typeOptionController = TypeOptionController(
field: field, field: field,
loader: loader, loader: loader,
), ),
super( _singleFieldListener = SingleFieldListener(fieldId: field.id),
FieldEditorState.initial( fieldService = FieldBackendService(
loader.viewId, viewId: viewId,
loader.field.name, fieldId: field.id,
isGroupField, ),
), fieldSettingsService = FieldSettingsBackendService(viewId: viewId),
) { super(FieldEditorState(field: FieldInfo.initial(field))) {
on<FieldEditorEvent>( on<FieldEditorEvent>(
(event, emit) async { (event, emit) async {
await event.when( await event.when(
initial: () async { initial: () async {
dataController.addFieldListener((field) { final fieldId = field.id;
typeOptionController.addFieldListener((field) {
if (!isClosed) { if (!isClosed) {
add(FieldEditorEvent.didReceiveFieldChanged(field)); add(FieldEditorEvent.didReceiveFieldChanged(fieldId));
} }
}); });
await dataController.reloadTypeOption(); _singleFieldListener.start(
add(FieldEditorEvent.didReceiveFieldChanged(dataController.field)); onFieldChanged: (field) {
}, if (!isClosed) {
updateName: (name) { add(FieldEditorEvent.didReceiveFieldChanged(fieldId));
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();
}, },
); );
await typeOptionController.reloadTypeOption();
add(FieldEditorEvent.didReceiveFieldChanged(fieldId));
}, },
switchToField: (FieldType fieldType) async { didReceiveFieldChanged: (fieldId) async {
await dataController.switchToField(fieldType); 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 @freezed
class FieldEditorEvent with _$FieldEditorEvent { class FieldEditorEvent with _$FieldEditorEvent {
const factory FieldEditorEvent.initial() = _InitialField; const factory FieldEditorEvent.initial() = _InitialField;
const factory FieldEditorEvent.updateName(String name) = _UpdateName; const factory FieldEditorEvent.didReceiveFieldChanged(String fieldId) =
const factory FieldEditorEvent.deleteField() = _DeleteField;
const factory FieldEditorEvent.switchToField(FieldType fieldType) =
_SwitchToField;
const factory FieldEditorEvent.didReceiveFieldChanged(FieldPB field) =
_DidReceiveFieldChanged; _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 @freezed
class FieldEditorState with _$FieldEditorState { class FieldEditorState with _$FieldEditorState {
const factory FieldEditorState({ const factory FieldEditorState({
required String viewId, required FieldInfo field,
required String errorText,
required String name,
required Option<FieldPB> field,
required bool canDelete,
required bool isGroupField,
}) = _FieldEditorState; }) = _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({ Future<Either<Unit, FlowyError>> updateField({
String? name, String? name,
bool? frozen, bool? frozen,
double? width,
}) { }) {
final payload = FieldChangesetPB.create() final payload = FieldChangesetPB.create()
..viewId = viewId ..viewId = viewId
@ -41,10 +40,6 @@ class FieldBackendService {
payload.frozen = frozen; payload.frozen = frozen;
} }
if (width != null) {
payload.width = width.toInt();
}
return DatabaseEventUpdateField(payload).send(); return DatabaseEventUpdateField(payload).send();
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -11,7 +11,7 @@ class GridSize {
static double get headerContainerPadding => 0 * scale; static double get headerContainerPadding => 0 * scale;
static double get cellHPadding => 10 * scale; static double get cellHPadding => 10 * scale;
static double get cellVPadding => 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 double get typeOptionSeparatorHeight => 4 * scale;
static EdgeInsets get headerContentInsets => EdgeInsets.symmetric( static EdgeInsets get headerContentInsets => EdgeInsets.symmetric(

View File

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

View File

@ -1,5 +1,6 @@
import 'package:appflowy/generated/flowy_svgs.g.dart'; 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_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/plugins/database_view/application/field/field_info.dart';
import 'package:appflowy_popover/appflowy_popover.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 'package:flutter_bloc/flutter_bloc.dart';
import '../../layout/sizes.dart'; import '../../layout/sizes.dart';
import 'field_cell_action_sheet.dart'; import 'field_editor.dart';
import 'field_type_extension.dart'; import 'field_type_extension.dart';
class GridFieldCell extends StatefulWidget { class GridFieldCell extends StatefulWidget {
final String viewId; final String viewId;
final FieldController fieldController;
final FieldInfo fieldInfo; final FieldInfo fieldInfo;
const GridFieldCell({ const GridFieldCell({
super.key, super.key,
required this.viewId, required this.viewId,
required this.fieldController,
required this.fieldInfo, required this.fieldInfo,
}); });
@ -59,9 +62,11 @@ class _GridFieldCellState extends State<GridFieldCell> {
direction: PopoverDirection.bottomWithLeftAligned, direction: PopoverDirection.bottomWithLeftAligned,
controller: popoverController, controller: popoverController,
popupBuilder: (BuildContext context) { popupBuilder: (BuildContext context) {
return GridFieldCellActionSheet( return FieldEditor(
viewId: widget.viewId, viewId: widget.viewId,
fieldInfo: widget.fieldInfo, fieldController: widget.fieldController,
field: widget.fieldInfo.field,
initialPage: FieldEditorPage.general,
); );
}, },
child: FieldCellButton( 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/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_editor_bloc.dart';
import 'package:appflowy/plugins/database_view/application/field/field_info.dart'; import 'package:appflowy/plugins/database_view/application/field/field_info.dart';
import 'package:appflowy/plugins/database_view/application/field/field_service.dart';
import 'package:appflowy/plugins/database_view/application/field/type_option/type_option_context.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:appflowy_popover/appflowy_popover.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.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:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:easy_localization/easy_localization.dart';
import '../../layout/sizes.dart'; import 'package:styled_widget/styled_widget.dart';
import 'field_type_option_editor.dart'; import 'field_type_option_editor.dart';
enum FieldEditorPage {
general,
details,
}
class FieldEditor extends StatefulWidget { class FieldEditor extends StatefulWidget {
final String viewId; final String viewId;
final bool isGroupingField; final FieldController fieldController;
final Function(String)? onDeleted; final FieldPB field;
final Function(String)? onToggleVisibility; final FieldEditorPage initialPage;
final FieldTypeOptionLoader typeOptionLoader;
final FieldInfo? fieldInfo;
const FieldEditor({ const FieldEditor({
required this.viewId,
required this.typeOptionLoader,
this.fieldInfo,
this.isGroupingField = false,
this.onDeleted,
this.onToggleVisibility,
super.key, super.key,
required this.viewId,
required this.field,
required this.fieldController,
this.initialPage = FieldEditorPage.details,
}); });
@override @override
@ -40,6 +43,227 @@ class FieldEditor extends StatefulWidget {
} }
class _FieldEditorState extends State<FieldEditor> { 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; late PopoverMutex popoverMutex;
@override @override
@ -56,71 +280,77 @@ class _FieldEditorState extends State<FieldEditor> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final bool requireSpace = widget.onDeleted != null ||
widget.onToggleVisibility != null ||
!widget.typeOptionLoader.field.isPrimary;
final List<Widget> children = [ final List<Widget> children = [
FieldNameTextField(popoverMutex: popoverMutex), FieldNameTextField(
if (requireSpace) const VSpace(4), popoverMutex: popoverMutex,
if (widget.onDeleted != null) _addDeleteFieldButton(), padding: const EdgeInsets.fromLTRB(12.0, 4.0, 12.0, 0.0),
if (widget.onToggleVisibility != null) _addHideFieldButton(), textEditingController: widget.textEditingController,
if (!widget.typeOptionLoader.field.isPrimary) ),
FieldTypeOptionCell(popoverMutex: popoverMutex), const VSpace(8),
FieldTypeOptionCell(popoverMutex: popoverMutex),
const TypeOptionSeparator(),
_addFieldVisibilityToggleButton(),
_addDuplicateFieldButton(),
_addDeleteFieldButton(),
]; ];
return BlocProvider(
create: (context) { return Padding(
return FieldEditorBloc( padding: const EdgeInsets.symmetric(vertical: 8.0),
isGroupField: widget.isGroupingField, child: Column(
loader: widget.typeOptionLoader, mainAxisSize: MainAxisSize.min,
field: widget.typeOptionLoader.field, children: children,
)..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),
), ),
); );
} }
Widget _addDeleteFieldButton() { Widget _addFieldVisibilityToggleButton() {
return BlocBuilder<FieldEditorBloc, FieldEditorState>( return BlocBuilder<FieldEditorBloc, FieldEditorState>(
builder: (context, state) { builder: (context, state) {
return Padding( return Padding(
padding: const EdgeInsets.symmetric(horizontal: 12.0), padding: const EdgeInsets.fromLTRB(8.0, 2.0, 8.0, 0),
child: DeleteFieldButton( child: FieldActionCell(
viewId: widget.viewId,
fieldInfo: state.field,
action: FieldAction.toggleVisibility,
popoverMutex: popoverMutex, 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>( return BlocBuilder<FieldEditorBloc, FieldEditorState>(
builder: (context, state) { builder: (context, state) {
if (state.field.isPrimary) {
return const SizedBox.shrink();
}
return Padding( return Padding(
padding: const EdgeInsets.symmetric(horizontal: 12.0), padding: const EdgeInsets.fromLTRB(8.0, 4.0, 8.0, 0),
child: FieldVisibilityToggleButton( child: FieldActionCell(
isFieldHidden: viewId: widget.viewId,
widget.fieldInfo!.visibility == FieldVisibility.AlwaysHidden, 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, 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; final PopoverMutex popoverMutex;
const FieldTypeOptionCell({ const FieldTypeOptionCell({
Key? key, super.key,
required this.popoverMutex, required this.popoverMutex,
}) : super(key: key); });
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return BlocBuilder<FieldEditorBloc, FieldEditorState>( return BlocBuilder<FieldEditorBloc, FieldEditorState>(
buildWhen: (p, c) => p.field != c.field,
builder: (context, state) { builder: (context, state) {
return state.field.fold( if (state.field.isPrimary) {
() => const SizedBox.shrink(), return const SizedBox.shrink();
(fieldInfo) { }
final dataController = final dataController =
context.read<FieldEditorBloc>().dataController; context.read<FieldEditorBloc>().typeOptionController;
return FieldTypeOptionEditor( return Padding(
dataController: dataController, padding: const EdgeInsets.only(bottom: 2.0),
popoverMutex: popoverMutex, child: FieldTypeOptionEditor(
); dataController: dataController,
}, popoverMutex: popoverMutex,
),
); );
}, },
); );
@ -158,18 +388,21 @@ class FieldTypeOptionCell extends StatelessWidget {
} }
class FieldNameTextField extends StatefulWidget { class FieldNameTextField extends StatefulWidget {
final PopoverMutex popoverMutex; final TextEditingController textEditingController;
final PopoverMutex? popoverMutex;
final EdgeInsets padding;
const FieldNameTextField({ const FieldNameTextField({
required this.popoverMutex, super.key,
Key? key, required this.textEditingController,
}) : super(key: key); this.popoverMutex,
this.padding = EdgeInsets.zero,
});
@override @override
State<FieldNameTextField> createState() => _FieldNameTextFieldState(); State<FieldNameTextField> createState() => _FieldNameTextFieldState();
} }
class _FieldNameTextFieldState extends State<FieldNameTextField> { class _FieldNameTextFieldState extends State<FieldNameTextField> {
final textController = TextEditingController();
FocusNode focusNode = FocusNode(); FocusNode focusNode = FocusNode();
@override @override
@ -178,11 +411,11 @@ class _FieldNameTextFieldState extends State<FieldNameTextField> {
focusNode.addListener(() { focusNode.addListener(() {
if (focusNode.hasFocus) { if (focusNode.hasFocus) {
widget.popoverMutex.close(); widget.popoverMutex?.close();
} }
}); });
widget.popoverMutex.listenOnPopoverChanged(() { widget.popoverMutex?.listenOnPopoverChanged(() {
if (focusNode.hasFocus) { if (focusNode.hasFocus) {
focusNode.unfocus(); focusNode.unfocus();
} }
@ -191,24 +424,18 @@ class _FieldNameTextFieldState extends State<FieldNameTextField> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return BlocBuilder<FieldEditorBloc, FieldEditorState>( return Padding(
builder: (context, state) { padding: widget.padding,
return Padding( child: FlowyTextField(
padding: const EdgeInsets.symmetric(horizontal: 12.0), focusNode: focusNode,
child: FlowyTextField( controller: widget.textEditingController,
focusNode: focusNode, onSubmitted: (_) => PopoverContainer.of(context).close(),
controller: textController, onChanged: (newName) {
onSubmitted: (String _) => PopoverContainer.of(context).close(), context
text: state.name, .read<FieldEditorBloc>()
errorText: state.errorText.isEmpty ? null : state.errorText, .add(FieldEditorEvent.renameField(newName));
onChanged: (newName) { },
context ),
.read<FieldEditorBloc>()
.add(FieldEditorEvent.updateName(newName));
},
),
);
},
); );
} }
@ -216,80 +443,10 @@ class _FieldNameTextFieldState extends State<FieldNameTextField> {
void dispose() { void dispose() {
focusNode.removeListener(() { focusNode.removeListener(() {
if (focusNode.hasFocus) { if (focusNode.hasFocus) {
widget.popoverMutex.close(); widget.popoverMutex?.close();
} }
}); });
focusNode.dispose(); focusNode.dispose();
super.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/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/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/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:appflowy_popover/appflowy_popover.dart';
import 'package:dartz/dartz.dart' show Either; 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:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import '../../layout/sizes.dart';
import 'field_type_extension.dart'; import 'field_type_extension.dart';
import 'field_type_list.dart'; import 'field_type_list.dart';
import 'type_option/builder.dart'; import 'type_option/builder.dart';
@ -27,19 +28,17 @@ class FieldTypeOptionEditor extends StatelessWidget {
final PopoverMutex popoverMutex; final PopoverMutex popoverMutex;
const FieldTypeOptionEditor({ const FieldTypeOptionEditor({
super.key,
required TypeOptionController dataController, required TypeOptionController dataController,
required this.popoverMutex, required this.popoverMutex,
Key? key, }) : _dataController = dataController;
}) : _dataController = dataController,
super(key: key);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return BlocProvider( return BlocProvider(
create: (context) { create: (context) {
final bloc = FieldTypeOptionEditBloc(_dataController); return FieldTypeOptionEditBloc(_dataController)
bloc.add(const FieldTypeOptionEditEvent.initial()); ..add(const FieldTypeOptionEditEvent.initial());
return bloc;
}, },
child: BlocBuilder<FieldTypeOptionEditBloc, FieldTypeOptionEditState>( child: BlocBuilder<FieldTypeOptionEditBloc, FieldTypeOptionEditState>(
builder: (context, state) { builder: (context, state) {
@ -53,8 +52,8 @@ class FieldTypeOptionEditor extends StatelessWidget {
if (typeOptionWidget != null) typeOptionWidget, if (typeOptionWidget != null) typeOptionWidget,
]; ];
return ListView( return Column(
shrinkWrap: true, mainAxisSize: MainAxisSize.min,
children: children, children: children,
); );
}, },
@ -74,22 +73,30 @@ class FieldTypeOptionEditor extends StatelessWidget {
} }
} }
class SwitchFieldButton extends StatelessWidget { class SwitchFieldButton extends StatefulWidget {
final PopoverMutex popoverMutex; final PopoverMutex popoverMutex;
const SwitchFieldButton({ const SwitchFieldButton({
super.key,
required this.popoverMutex, required this.popoverMutex,
Key? key, });
}) : super(key: key);
@override
State<SwitchFieldButton> createState() => _SwitchFieldButtonState();
}
class _SwitchFieldButtonState extends State<SwitchFieldButton> {
final PopoverController _popoverController = PopoverController();
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final widget = AppFlowyPopover( final child = AppFlowyPopover(
constraints: BoxConstraints.loose(const Size(460, 540)), constraints: BoxConstraints.loose(const Size(460, 540)),
asBarrier: true, triggerActions: PopoverTriggerFlags.hover,
triggerActions: PopoverTriggerFlags.click, mutex: widget.popoverMutex,
mutex: popoverMutex, controller: _popoverController,
offset: const Offset(8, 0), offset: const Offset(8, 0),
popupBuilder: (popOverContext) { margin: const EdgeInsets.all(8),
popupBuilder: (BuildContext popoverContext) {
return FieldTypeList( return FieldTypeList(
onSelectField: (newFieldType) { onSelectField: (newFieldType) {
context context
@ -99,20 +106,21 @@ class SwitchFieldButton extends StatelessWidget {
); );
}, },
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 12.0), padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: _buildMoreButton(context), child: _buildMoreButton(context),
), ),
); );
return SizedBox( return SizedBox(
height: GridSize.popoverItemHeight, height: GridSize.popoverItemHeight,
child: widget, child: child,
); );
} }
Widget _buildMoreButton(BuildContext context) { Widget _buildMoreButton(BuildContext context) {
final bloc = context.read<FieldTypeOptionEditBloc>(); final bloc = context.read<FieldTypeOptionEditBloc>();
return FlowyButton( return FlowyButton(
onTap: () => _popoverController.show(),
text: FlowyText.medium( text: FlowyText.medium(
bloc.state.field.fieldType.title(), bloc.state.field.fieldType.title(),
), ),

View File

@ -1,7 +1,7 @@
import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/database_view/application/field/field_controller.dart'; import 'package:appflowy/plugins/database_view/application/field/field_controller.dart';
import 'package:appflowy/plugins/database_view/application/field/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/application/grid_header_bloc.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/mobile_field_cell.dart'; import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/mobile_field_cell.dart';
import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/log.dart';
@ -22,15 +22,13 @@ import 'field_cell.dart';
class GridHeaderSliverAdaptor extends StatefulWidget { class GridHeaderSliverAdaptor extends StatefulWidget {
final String viewId; final String viewId;
final FieldController fieldController;
final ScrollController anchorScrollController; final ScrollController anchorScrollController;
const GridHeaderSliverAdaptor({ const GridHeaderSliverAdaptor({
required this.viewId, required this.viewId,
required this.fieldController,
required this.anchorScrollController, required this.anchorScrollController,
Key? key, super.key,
}) : super(key: key); });
@override @override
State<GridHeaderSliverAdaptor> createState() => State<GridHeaderSliverAdaptor> createState() =>
@ -40,11 +38,13 @@ class GridHeaderSliverAdaptor extends StatefulWidget {
class _GridHeaderSliverAdaptorState extends State<GridHeaderSliverAdaptor> { class _GridHeaderSliverAdaptorState extends State<GridHeaderSliverAdaptor> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final fieldController =
context.read<GridBloc>().databaseController.fieldController;
return BlocProvider( return BlocProvider(
create: (context) { create: (context) {
return GridHeaderBloc( return GridHeaderBloc(
viewId: widget.viewId, viewId: widget.viewId,
fieldController: widget.fieldController, fieldController: fieldController,
)..add(const GridHeaderEvent.initial()); )..add(const GridHeaderEvent.initial());
}, },
child: BlocBuilder<GridHeaderBloc, GridHeaderState>( child: BlocBuilder<GridHeaderBloc, GridHeaderState>(
@ -54,7 +54,10 @@ class _GridHeaderSliverAdaptorState extends State<GridHeaderSliverAdaptor> {
return SingleChildScrollView( return SingleChildScrollView(
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,
controller: widget.anchorScrollController, 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 { class _GridHeader extends StatefulWidget {
final String viewId; 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 @override
State<_GridHeader> createState() => _GridHeaderState(); State<_GridHeader> createState() => _GridHeaderState();
@ -98,6 +102,7 @@ class _GridHeaderState extends State<_GridHeader> {
key: _getKeyById(fieldInfo.id), key: _getKeyById(fieldInfo.id),
viewId: widget.viewId, viewId: widget.viewId,
fieldInfo: fieldInfo, fieldInfo: fieldInfo,
fieldController: widget.fieldController,
) )
: MobileFieldButton( : MobileFieldButton(
key: _getKeyById(fieldInfo.id), key: _getKeyById(fieldInfo.id),
@ -159,8 +164,9 @@ class _CellLeading extends StatelessWidget {
} }
class _CellTrailing extends StatelessWidget { class _CellTrailing extends StatelessWidget {
const _CellTrailing({required this.viewId});
final String viewId; final String viewId;
const _CellTrailing({required this.viewId, Key? key}) : super(key: key);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -179,7 +185,10 @@ class _CellTrailing extends StatelessWidget {
class CreateFieldButton extends StatefulWidget { class CreateFieldButton extends StatefulWidget {
final String viewId; final String viewId;
const CreateFieldButton({required this.viewId, Key? key}) : super(key: key); const CreateFieldButton({
super.key,
required this.viewId,
});
@override @override
State<CreateFieldButton> createState() => _CreateFieldButtonState(); State<CreateFieldButton> createState() => _CreateFieldButtonState();
@ -191,6 +200,8 @@ class _CreateFieldButtonState extends State<CreateFieldButton> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final fieldController =
context.read<GridBloc>().databaseController.fieldController;
return AppFlowyPopover( return AppFlowyPopover(
controller: popoverController, controller: popoverController,
direction: PopoverDirection.bottomWithRightAligned, direction: PopoverDirection.bottomWithRightAligned,
@ -222,49 +233,13 @@ class _CreateFieldButtonState extends State<CreateFieldButton> {
}, },
leftIcon: const FlowySvg(FlowySvgs.add_s), leftIcon: const FlowySvg(FlowySvgs.add_s),
), ),
popupBuilder: (BuildContext popover) { popupBuilder: (BuildContext popoverContext) {
return FieldEditor( return FieldEditor(
viewId: widget.viewId, viewId: widget.viewId,
typeOptionLoader: FieldTypeOptionLoader( fieldController: fieldController,
viewId: widget.viewId, field: typeOption.field_2,
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/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/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/theme_extension.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'field_type_extension.dart'; import 'field_type_extension.dart';
@ -29,6 +33,8 @@ class MobileFieldButton extends StatelessWidget {
color: Theme.of(context).dividerColor, color: Theme.of(context).dividerColor,
width: 1.0, width: 1.0,
); );
final fieldController =
context.read<GridBloc>().databaseController.fieldController;
return Container( return Container(
width: field.fieldSettings!.width.toDouble(), width: field.fieldSettings!.width.toDouble(),
decoration: BoxDecoration( decoration: BoxDecoration(
@ -36,7 +42,14 @@ class MobileFieldButton extends StatelessWidget {
), ),
child: TextButton( child: TextButton(
onLongPress: () { onLongPress: () {
debugPrint("gimme the bottom drawer"); showMobileBottomSheet(
context: context,
builder: (context) => MobileDBBottomSheetFieldEditor(
viewId: viewId,
field: field.field,
fieldController: fieldController,
),
);
}, },
style: TextButton.styleFrom( style: TextButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 8), padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 8),

View File

@ -42,11 +42,12 @@ class NumberTypeOptionWidgetBuilder extends TypeOptionWidgetBuilder {
class NumberTypeOptionWidget extends TypeOptionWidget { class NumberTypeOptionWidget extends TypeOptionWidget {
final NumberTypeOptionContext typeOptionContext; final NumberTypeOptionContext typeOptionContext;
final PopoverMutex popoverMutex; final PopoverMutex popoverMutex;
const NumberTypeOptionWidget({ const NumberTypeOptionWidget({
super.key,
required this.typeOptionContext, required this.typeOptionContext,
required this.popoverMutex, required this.popoverMutex,
Key? key, });
}) : super(key: key);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -60,7 +61,6 @@ class NumberTypeOptionWidget extends TypeOptionWidget {
final selectNumUnitButton = SizedBox( final selectNumUnitButton = SizedBox(
height: GridSize.popoverItemHeight, height: GridSize.popoverItemHeight,
child: FlowyButton( child: FlowyButton(
margin: GridSize.typeOptionContentInsets,
rightIcon: const FlowySvg(FlowySvgs.more_s), rightIcon: const FlowySvg(FlowySvgs.more_s),
text: FlowyText.regular( text: FlowyText.regular(
state.typeOption.format.title(), state.typeOption.format.title(),
@ -72,13 +72,15 @@ class NumberTypeOptionWidget extends TypeOptionWidget {
padding: const EdgeInsets.only(left: 6), padding: const EdgeInsets.only(left: 6),
height: GridSize.popoverItemHeight, height: GridSize.popoverItemHeight,
alignment: Alignment.centerLeft, alignment: Alignment.centerLeft,
child: FlowyText.medium( child: FlowyText.regular(
color: Theme.of(context).colorScheme.outline,
LocaleKeys.grid_field_numberFormat.tr(), LocaleKeys.grid_field_numberFormat.tr(),
color: Theme.of(context).hintColor,
fontSize: 11,
), ),
); );
return Padding( return Padding(
padding: const EdgeInsets.symmetric(horizontal: 12), padding: const EdgeInsets.symmetric(horizontal: 8),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
@ -87,7 +89,7 @@ class NumberTypeOptionWidget extends TypeOptionWidget {
mutex: popoverMutex, mutex: popoverMutex,
triggerActions: triggerActions:
PopoverTriggerFlags.hover | PopoverTriggerFlags.click, PopoverTriggerFlags.hover | PopoverTriggerFlags.click,
offset: const Offset(8, 0), offset: const Offset(16, 0),
constraints: BoxConstraints.loose(const Size(460, 440)), constraints: BoxConstraints.loose(const Size(460, 440)),
margin: EdgeInsets.zero, margin: EdgeInsets.zero,
child: selectNumUnitButton, 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_backend/protobuf/flowy-database2/select_option.pb.dart';
import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
@ -40,23 +39,21 @@ class SelectOptionTypeOptionWidget extends StatelessWidget {
BlocBuilder<SelectOptionTypeOptionBloc, SelectOptionTypeOptionState>( BlocBuilder<SelectOptionTypeOptionBloc, SelectOptionTypeOptionState>(
builder: (context, state) { builder: (context, state) {
final List<Widget> children = [ final List<Widget> children = [
const TypeOptionSeparator(), const TypeOptionSeparator(spacing: 8),
const OptionTitle(), const _OptionTitle(),
if (state.isEditingOption) const VSpace(4),
if (state.isEditingOption) ...[
CreateOptionTextField(popoverMutex: popoverMutex), CreateOptionTextField(popoverMutex: popoverMutex),
if (state.options.isNotEmpty && state.isEditingOption) const VSpace(4),
const VSpace(10), ] else
if (state.options.isEmpty && !state.isEditingOption)
const _AddOptionButton(), const _AddOptionButton(),
const VSpace(4),
_OptionList(popoverMutex: popoverMutex), _OptionList(popoverMutex: popoverMutex),
]; ];
return ListView.builder( return Column(
shrinkWrap: true, mainAxisSize: MainAxisSize.min,
itemCount: children.length, children: children,
itemBuilder: (context, index) {
return children[index];
},
); );
}, },
), ),
@ -64,31 +61,22 @@ class SelectOptionTypeOptionWidget extends StatelessWidget {
} }
} }
class OptionTitle extends StatelessWidget { class _OptionTitle extends StatelessWidget {
const OptionTitle({Key? key}) : super(key: key); const _OptionTitle();
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return BlocBuilder<SelectOptionTypeOptionBloc, SelectOptionTypeOptionState>( return BlocBuilder<SelectOptionTypeOptionBloc, SelectOptionTypeOptionState>(
builder: (context, state) { 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( return Padding(
padding: const EdgeInsets.symmetric(horizontal: 12.0), padding: const EdgeInsets.symmetric(horizontal: 16),
child: SizedBox( child: Align(
height: GridSize.popoverItemHeight, alignment: AlignmentDirectional.centerStart,
child: Row(children: children), 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 { class _OptionList extends StatelessWidget {
final PopoverMutex? popoverMutex; final PopoverMutex? popoverMutex;
const _OptionList({Key? key, this.popoverMutex}) : super(key: key); const _OptionList({Key? key, this.popoverMutex}) : super(key: key);
@ -140,7 +105,6 @@ class _OptionList extends StatelessWidget {
return ListView.separated( return ListView.separated(
shrinkWrap: true, shrinkWrap: true,
controller: ScrollController(),
separatorBuilder: (context, index) { separatorBuilder: (context, index) {
return VSpace(GridSize.typeOptionSeparatorHeight); return VSpace(GridSize.typeOptionSeparatorHeight);
}, },
@ -187,7 +151,7 @@ class _OptionCellState extends State<_OptionCell> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final child = SizedBox( final child = SizedBox(
height: GridSize.popoverItemHeight, height: 28,
child: SelectOptionTagCell( child: SelectOptionTagCell(
option: widget.option, option: widget.option,
onSelected: (SelectOptionPB pb) { onSelected: (SelectOptionPB pb) {
@ -212,7 +176,7 @@ class _OptionCellState extends State<_OptionCell> {
asBarrier: true, asBarrier: true,
constraints: BoxConstraints.loose(const Size(460, 470)), constraints: BoxConstraints.loose(const Size(460, 470)),
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 12.0), padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: child, child: child,
), ),
popupBuilder: (BuildContext popoverContext) { popupBuilder: (BuildContext popoverContext) {
@ -243,24 +207,19 @@ class _AddOptionButton extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Padding( return Padding(
padding: const EdgeInsets.symmetric(horizontal: 12.0), padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: SizedBox( child: SizedBox(
height: GridSize.popoverItemHeight, height: GridSize.popoverItemHeight,
child: FlowyButton( child: FlowyButton(
hoverColor: AFThemeExtension.of(context).lightGreyHover,
text: FlowyText.medium( text: FlowyText.medium(
LocaleKeys.grid_field_addSelectOption.tr(), LocaleKeys.grid_field_addSelectOption.tr(),
color: AFThemeExtension.of(context).textColor,
), ),
onTap: () { onTap: () {
context context
.read<SelectOptionTypeOptionBloc>() .read<SelectOptionTypeOptionBloc>()
.add(const SelectOptionTypeOptionEvent.addingOption()); .add(const SelectOptionTypeOptionEvent.addingOption());
}, },
leftIcon: FlowySvg( leftIcon: const FlowySvg(FlowySvgs.add_s),
FlowySvgs.add_s,
color: Theme.of(context).iconTheme.color,
),
), ),
), ),
); );
@ -303,7 +262,7 @@ class _CreateOptionTextFieldState extends State<CreateOptionTextField> {
builder: (context, state) { builder: (context, state) {
final text = state.newOptionName.foldRight("", (a, previous) => a); final text = state.newOptionName.foldRight("", (a, previous) => a);
return Padding( return Padding(
padding: const EdgeInsets.symmetric(horizontal: 12.0), padding: const EdgeInsets.symmetric(horizontal: 14.0),
child: FlowyTextField( child: FlowyTextField(
autoClearWhenDone: true, autoClearWhenDone: true,
text: text, text: text,

View File

@ -62,20 +62,14 @@ class SelectOptionTypeOptionEditor extends StatelessWidget {
), ),
const VSpace(10), const VSpace(10),
const _DeleteTag(), 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( return SizedBox(
width: 180, width: 180,
child: ListView.builder( child: ListView.builder(
@ -188,7 +182,6 @@ class SelectOptionColorList extends StatelessWidget {
), ),
ListView.separated( ListView.separated(
shrinkWrap: true, shrinkWrap: true,
controller: ScrollController(),
separatorBuilder: (context, index) { separatorBuilder: (context, index) {
return VSpace(GridSize.typeOptionSeparatorHeight); return VSpace(GridSize.typeOptionSeparatorHeight);
}, },

View File

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

View File

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

View File

@ -109,7 +109,6 @@ class _OptionList extends StatelessWidget {
return ListView.separated( return ListView.separated(
shrinkWrap: true, shrinkWrap: true,
controller: ScrollController(),
itemCount: cells.length, itemCount: cells.length,
separatorBuilder: (_, __) => separatorBuilder: (_, __) =>
VSpace(GridSize.typeOptionSeparatorHeight), VSpace(GridSize.typeOptionSeparatorHeight),
@ -302,7 +301,7 @@ class _SelectOptionCellState extends State<_SelectOptionCell> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final child = SizedBox( final child = SizedBox(
height: GridSize.popoverItemHeight, height: 28,
child: SelectOptionTagCell( child: SelectOptionTagCell(
option: widget.option, option: widget.option,
onSelected: (option) { onSelected: (option) {
@ -328,7 +327,6 @@ class _SelectOptionCellState extends State<_SelectOptionCell> {
FlowyIconButton( FlowyIconButton(
onPressed: () => _popoverController.show(), onPressed: () => _popoverController.show(),
iconPadding: const EdgeInsets.symmetric(horizontal: 6.0), 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, hoverColor: Colors.transparent,
icon: FlowySvg( icon: FlowySvg(
FlowySvgs.details_s, 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/application/row/row_controller.dart';
import 'package:appflowy/plugins/database_view/grid/application/row/row_detail_bloc.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'; import 'package:appflowy/plugins/database_view/widgets/row/row_document.dart';
@ -11,11 +12,13 @@ import 'row_banner.dart';
import 'row_property.dart'; import 'row_property.dart';
class RowDetailPage extends StatefulWidget with FlowyOverlayDelegate { class RowDetailPage extends StatefulWidget with FlowyOverlayDelegate {
final FieldController fieldController;
final RowController rowController; final RowController rowController;
final GridCellBuilder cellBuilder; final GridCellBuilder cellBuilder;
const RowDetailPage({ const RowDetailPage({
super.key, super.key,
required this.fieldController,
required this.rowController, required this.rowController,
required this.cellBuilder, required this.cellBuilder,
}); });
@ -56,6 +59,7 @@ class _RowDetailPageState extends State<RowDetailPage> {
child: RowPropertyList( child: RowPropertyList(
cellBuilder: widget.cellBuilder, cellBuilder: widget.cellBuilder,
viewId: widget.rowController.viewId, viewId: widget.rowController.viewId,
fieldController: widget.fieldController,
), ),
), ),
const VSpace(20), const VSpace(20),

View File

@ -3,14 +3,13 @@ import 'dart:io';
import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/database_view/application/cell/cell_service.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/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/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_cell.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_editor.dart';
import 'package:appflowy/plugins/database_view/widgets/row/cells/cells.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/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/log.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart';
import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:appflowy_popover/appflowy_popover.dart';
@ -28,12 +27,15 @@ import 'cell_builder.dart';
/// [RowDetailPage]. /// [RowDetailPage].
class RowPropertyList extends StatelessWidget { class RowPropertyList extends StatelessWidget {
final String viewId; final String viewId;
final FieldController fieldController;
final GridCellBuilder cellBuilder; final GridCellBuilder cellBuilder;
const RowPropertyList({ const RowPropertyList({
super.key,
required this.viewId, required this.viewId,
required this.fieldController,
required this.cellBuilder, required this.cellBuilder,
Key? key, });
}) : super(key: key);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -46,6 +48,7 @@ class RowPropertyList extends StatelessWidget {
key: ValueKey('row_detail_${cell.fieldId}'), key: ValueKey('row_detail_${cell.fieldId}'),
cellContext: cell, cellContext: cell,
cellBuilder: cellBuilder, cellBuilder: cellBuilder,
fieldController: fieldController,
index: index, index: index,
), ),
) )
@ -99,7 +102,10 @@ class RowPropertyList extends StatelessWidget {
padding: EdgeInsets.only(bottom: 4.0), padding: EdgeInsets.only(bottom: 4.0),
child: ToggleHiddenFieldsVisibilityButton(), child: ToggleHiddenFieldsVisibilityButton(),
), ),
CreateRowFieldButton(viewId: viewId), CreateRowFieldButton(
viewId: viewId,
fieldController: fieldController,
),
], ],
), ),
), ),
@ -113,14 +119,16 @@ class RowPropertyList extends StatelessWidget {
class _PropertyCell extends StatefulWidget { class _PropertyCell extends StatefulWidget {
final DatabaseCellContext cellContext; final DatabaseCellContext cellContext;
final GridCellBuilder cellBuilder; final GridCellBuilder cellBuilder;
final FieldController fieldController;
final int index; final int index;
const _PropertyCell({ const _PropertyCell({
super.key,
required this.cellContext, required this.cellContext,
required this.cellBuilder, required this.cellBuilder,
Key? key, required this.fieldController,
required this.index, required this.index,
}) : super(key: key); });
@override @override
State<StatefulWidget> createState() => _PropertyCellState(); State<StatefulWidget> createState() => _PropertyCellState();
@ -219,30 +227,8 @@ class _PropertyCellState extends State<_PropertyCell> {
Widget buildFieldEditor() { Widget buildFieldEditor() {
return FieldEditor( return FieldEditor(
viewId: widget.cellContext.viewId, viewId: widget.cellContext.viewId,
fieldInfo: widget.cellContext.fieldInfo, field: widget.cellContext.fieldInfo.field,
isGroupingField: widget.cellContext.fieldInfo.isGroupField, fieldController: widget.fieldController,
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);
},
); );
} }
} }
@ -342,8 +328,13 @@ class ToggleHiddenFieldsVisibilityButton extends StatelessWidget {
class CreateRowFieldButton extends StatefulWidget { class CreateRowFieldButton extends StatefulWidget {
final String viewId; final String viewId;
final FieldController fieldController;
const CreateRowFieldButton({required this.viewId, super.key}); const CreateRowFieldButton({
super.key,
required this.viewId,
required this.fieldController,
});
@override @override
State<CreateRowFieldButton> createState() => _CreateRowFieldButtonState(); State<CreateRowFieldButton> createState() => _CreateRowFieldButtonState();
@ -394,24 +385,11 @@ class _CreateRowFieldButtonState extends State<CreateRowFieldButton> {
), ),
), ),
), ),
popupBuilder: (BuildContext popOverContext) { popupBuilder: (BuildContext popoverContext) {
return FieldEditor( return FieldEditor(
viewId: widget.viewId, viewId: widget.viewId,
typeOptionLoader: FieldTypeOptionLoader( field: typeOption.field_2,
viewId: widget.viewId, fieldController: widget.fieldController,
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);
},
); );
}, },
); );

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -13,7 +13,8 @@ Future<FieldEditorBloc> createEditorBloc(AppFlowyGridTest gridTest) async {
); );
return FieldEditorBloc( return FieldEditorBloc(
isGroupField: fieldInfo.isGroupField, viewId: context.gridView.id,
fieldController: context.fieldController,
loader: loader, loader: loader,
field: fieldInfo.field, field: fieldInfo.field,
)..add(const FieldEditorEvent.initial()); )..add(const FieldEditorEvent.initial());
@ -27,67 +28,21 @@ void main() {
}); });
test('rename field', () async { test('rename field', () async {
final editorBloc = await makeEditorBloc(gridTest); final editorBloc = await createEditorBloc(gridTest);
editorBloc.add(const FieldEditorEvent.updateName('Hello world')); editorBloc.add(const FieldEditorEvent.renameField('Hello world'));
await gridResponseFuture();
editorBloc.state.field.fold( await gridResponseFuture();
() => throw Exception("The field should not be none"), expect(editorBloc.state.field.name, equals("Hello world"));
(field) {
assert(field.name == 'Hello world');
},
);
}); });
test('switch to text field', () async { 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(); await gridResponseFuture();
editorBloc.state.field.fold( // The default length of the fields is 3. The length of the fields
() => throw Exception("The field should not be none"), // should not change after switching to other field type
(field) { expect(editorBloc.state.field.fieldType, equals(FieldType.RichText));
// 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);
},
);
}); });
} }
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 { Future<FieldEditorBloc> createField(FieldType fieldType) async {
final editorBloc = await createFieldEditor(viewId: gridView.id) final editorBloc =
..add(const FieldEditorEvent.initial()); await createFieldEditor(databaseController: gridController)
..add(const FieldEditorEvent.initial());
await gridResponseFuture(); await gridResponseFuture();
editorBloc.add(FieldEditorEvent.switchToField(fieldType)); editorBloc.add(FieldEditorEvent.switchFieldType(fieldType));
await gridResponseFuture(); await gridResponseFuture();
return Future(() => editorBloc); return Future(() => editorBloc);
} }
@ -132,19 +133,21 @@ class GridTestContext {
} }
Future<FieldEditorBloc> createFieldEditor({ Future<FieldEditorBloc> createFieldEditor({
required String viewId, required DatabaseController databaseController,
}) async { }) async {
final result = await TypeOptionBackendService.createFieldTypeOption( final result = await TypeOptionBackendService.createFieldTypeOption(
viewId: viewId, viewId: databaseController.viewId,
); );
await gridResponseFuture();
return result.fold( return result.fold(
(data) { (data) {
final loader = FieldTypeOptionLoader( final loader = FieldTypeOptionLoader(
viewId: viewId, viewId: databaseController.viewId,
field: data.field_2, field: data.field_2,
); );
return FieldEditorBloc( return FieldEditorBloc(
isGroupField: FieldInfo.initial(data.field_2).isGroupField, viewId: databaseController.viewId,
fieldController: databaseController.fieldController,
loader: loader, loader: loader,
field: data.field_2, field: data.field_2,
); );