diff --git a/frontend/appflowy_flutter/integration_test/desktop/board/board_field_test.dart b/frontend/appflowy_flutter/integration_test/desktop/board/board_field_test.dart index 3fe48b5f6f..387c35adf2 100644 --- a/frontend/appflowy_flutter/integration_test/desktop/board/board_field_test.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/board/board_field_test.dart @@ -22,7 +22,7 @@ void main() { const fieldName = "test change field"; await tester.createField( FieldType.RichText, - fieldName, + name: fieldName, layout: ViewLayoutPB.Board, ); await tester.tapButton(card1); diff --git a/frontend/appflowy_flutter/integration_test/desktop/database/database_cell_test.dart b/frontend/appflowy_flutter/integration_test/desktop/database/database_cell_test.dart index 66ef6cfd95..28f50bf817 100644 --- a/frontend/appflowy_flutter/integration_test/desktop/database/database_cell_test.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/database/database_cell_test.dart @@ -1,3 +1,4 @@ +import 'package:appflowy/util/field_type_extension.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -41,7 +42,7 @@ void main() { name: 'my grid', layout: ViewLayoutPB.Grid, ); - await tester.createField(FieldType.RichText, 'description'); + await tester.createField(FieldType.RichText, name: 'description'); await tester.editCell( rowIndex: 0, @@ -81,7 +82,7 @@ void main() { const fieldType = FieldType.Number; // Create a number field - await tester.createField(fieldType, fieldType.name); + await tester.createField(fieldType); await tester.editCell( rowIndex: 0, @@ -157,7 +158,7 @@ void main() { const fieldType = FieldType.CreatedTime; // Create a create time field // The create time field is not editable - await tester.createField(fieldType, fieldType.name); + await tester.createField(fieldType); await tester.tapCellInGrid(rowIndex: 0, fieldType: fieldType); @@ -175,7 +176,7 @@ void main() { const fieldType = FieldType.LastEditedTime; // Create a last time field // The last time field is not editable - await tester.createField(fieldType, fieldType.name); + await tester.createField(fieldType); await tester.tapCellInGrid(rowIndex: 0, fieldType: fieldType); @@ -191,7 +192,7 @@ void main() { await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid); const fieldType = FieldType.DateTime; - await tester.createField(fieldType, fieldType.name); + await tester.createField(fieldType); // Tap the cell to invoke the field editor await tester.tapCellInGrid(rowIndex: 0, fieldType: fieldType); @@ -366,7 +367,7 @@ void main() { await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid); const fieldType = FieldType.MultiSelect; - await tester.createField(fieldType, fieldType.name); + await tester.createField(fieldType, name: fieldType.i18n); // Tap the cell to invoke the selection option editor await tester.tapSelectOptionCellInGrid(rowIndex: 0, fieldType: fieldType); @@ -449,7 +450,7 @@ void main() { await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid); const fieldType = FieldType.Checklist; - await tester.createField(fieldType, fieldType.name); + await tester.createField(fieldType); // assert that there is no progress bar in the grid tester.assertChecklistCellInGrid(rowIndex: 0, percent: null); diff --git a/frontend/appflowy_flutter/integration_test/desktop/database/database_field_test.dart b/frontend/appflowy_flutter/integration_test/desktop/database/database_field_test.dart index cc1187da21..45d05207ff 100644 --- a/frontend/appflowy_flutter/integration_test/desktop/database/database_field_test.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/database/database_field_test.dart @@ -1,12 +1,13 @@ +import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/database/grid/presentation/grid_page.dart'; import 'package:appflowy/plugins/database/widgets/field/type_option_editor/select/select_option.dart'; import 'package:appflowy/util/field_type_extension.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart'; +import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; -import 'package:intl/intl.dart'; import '../../shared/database_test_op.dart'; import '../../shared/util.dart'; @@ -56,11 +57,22 @@ void main() { await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid); // create a field - await tester.createField(FieldType.Checklist, 'checklist'); + await tester.createField(FieldType.Checklist); + tester.findFieldWithName(FieldType.Checklist.i18n); - // check the field is created successfully - tester.findFieldWithName('checklist'); - await tester.pumpAndSettle(); + // editing field type during field creation should change title + await tester.createField(FieldType.MultiSelect); + tester.findFieldWithName(FieldType.MultiSelect.i18n); + + // not if the user changes the title manually though + const name = "New field"; + await tester.createField(FieldType.DateTime); + await tester.tapGridFieldWithName(FieldType.DateTime.i18n); + await tester.renameField(name); + await tester.tapEditFieldButton(); + await tester.tapSwitchFieldTypeButton(); + await tester.selectFieldType(FieldType.URL); + tester.findFieldWithName(name); }); testWidgets('delete field', (tester) async { @@ -70,14 +82,14 @@ void main() { await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid); // create a field - await tester.createField(FieldType.Checkbox, 'New field 1'); + await tester.createField(FieldType.Checkbox, name: 'New field 1'); // Delete the field await tester.tapGridFieldWithName('New field 1'); await tester.tapDeletePropertyButton(); // confirm delete - await tester.tapDialogOkButton(); + await tester.tapButtonWithName(LocaleKeys.space_delete.tr()); tester.noFieldWithName('New field 1'); await tester.pumpAndSettle(); @@ -90,10 +102,7 @@ void main() { await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid); // create a field - await tester.scrollToRight(find.byType(GridPage)); - await tester.tapNewPropertyButton(); - await tester.renameField('New field 1'); - await tester.dismissFieldEditor(); + await tester.createField(FieldType.RichText, name: 'New field 1'); // duplicate the field await tester.tapGridFieldWithName('New field 1'); @@ -126,26 +135,6 @@ void main() { await tester.pumpAndSettle(); }); - testWidgets('create checklist field', (tester) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - - await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid); - - await tester.scrollToRight(find.byType(GridPage)); - await tester.tapNewPropertyButton(); - - // Open the type option menu - await tester.tapSwitchFieldTypeButton(); - - await tester.selectFieldType(FieldType.Checklist); - - // After update the field type, the cells should be updated - await tester.findCellByFieldType(FieldType.Checklist); - - await tester.pumpAndSettle(); - }); - testWidgets('create list of fields', (tester) async { await tester.initializeAppFlowy(); await tester.tapAnonymousSignInButton(); @@ -162,18 +151,10 @@ void main() { FieldType.CreatedTime, FieldType.Checkbox, ]) { - await tester.scrollToRight(find.byType(GridPage)); - await tester.tapNewPropertyButton(); - await tester.renameField(fieldType.name); - - // Open the type option menu - await tester.tapSwitchFieldTypeButton(); - - await tester.selectFieldType(fieldType); - await tester.dismissFieldEditor(); + await tester.createField(fieldType); // After update the field type, the cells should be updated - await tester.findCellByFieldType(fieldType); + tester.findCellByFieldType(fieldType); await tester.pumpAndSettle(); } }); @@ -190,15 +171,7 @@ void main() { FieldType.Checklist, FieldType.URL, ]) { - // create the field - await tester.scrollToRight(find.byType(GridPage)); - await tester.tapNewPropertyButton(); - await tester.renameField(fieldType.i18n); - - // change field type - await tester.tapSwitchFieldTypeButton(); - await tester.selectFieldType(fieldType); - await tester.dismissFieldEditor(); + await tester.createField(fieldType); // open the field editor await tester.tapGridFieldWithName(fieldType.i18n); @@ -218,11 +191,7 @@ void main() { await tester.scrollToRight(find.byType(GridPage)); // create a number field - await tester.tapNewPropertyButton(); - await tester.renameField("Number"); - await tester.tapSwitchFieldTypeButton(); - await tester.selectFieldType(FieldType.Number); - await tester.dismissFieldEditor(); + await tester.createField(FieldType.Number); // enter some data into the first number cell await tester.editCell( @@ -243,7 +212,7 @@ void main() { ); // open editor and change number format - await tester.tapGridFieldWithName('Number'); + await tester.tapGridFieldWithName(FieldType.Number.i18n); await tester.tapEditFieldButton(); await tester.changeNumberFieldFormat(); await tester.dismissFieldEditor(); @@ -292,11 +261,7 @@ void main() { await tester.scrollToRight(find.byType(GridPage)); // create a date field - await tester.tapNewPropertyButton(); - await tester.renameField(FieldType.DateTime.i18n); - await tester.tapSwitchFieldTypeButton(); - await tester.selectFieldType(FieldType.DateTime); - await tester.dismissFieldEditor(); + await tester.createField(FieldType.DateTime); // edit the first date cell await tester.tapCellInGrid(rowIndex: 0, fieldType: FieldType.DateTime); diff --git a/frontend/appflowy_flutter/integration_test/desktop/database/database_row_page_test.dart b/frontend/appflowy_flutter/integration_test/desktop/database/database_row_page_test.dart index 0934e7721b..095d633b2d 100644 --- a/frontend/appflowy_flutter/integration_test/desktop/database/database_row_page_test.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/database/database_row_page_test.dart @@ -1,3 +1,6 @@ +import 'package:appflowy/plugins/database/grid/presentation/widgets/header/desktop_field_cell.dart'; +import 'package:appflowy/plugins/database/widgets/row/row_detail.dart'; +import 'package:appflowy/util/field_type_extension.dart'; import 'package:flutter/material.dart'; import 'package:appflowy/plugins/database/widgets/row/row_banner.dart'; @@ -121,15 +124,24 @@ void main() { FieldType.Checkbox, ]) { await tester.tapRowDetailPageCreatePropertyButton(); - await tester.renameField(fieldType.name); // Open the type option menu await tester.tapSwitchFieldTypeButton(); await tester.selectFieldType(fieldType); + final field = find.descendant( + of: find.byType(RowDetailPage), + matching: find.byWidgetPredicate( + (widget) => + widget is FieldCellButton && + widget.field.name == fieldType.i18n, + ), + ); + expect(field, findsOneWidget); + // After update the field type, the cells should be updated - await tester.findCellByFieldType(fieldType); + tester.findCellByFieldType(fieldType); await tester.scrollRowDetailByOffset(const Offset(0, -50)); } }); diff --git a/frontend/appflowy_flutter/integration_test/shared/database_test_op.dart b/frontend/appflowy_flutter/integration_test/shared/database_test_op.dart index 220673bd8a..b25fb056a5 100644 --- a/frontend/appflowy_flutter/integration_test/shared/database_test_op.dart +++ b/frontend/appflowy_flutter/integration_test/shared/database_test_op.dart @@ -74,7 +74,6 @@ import 'package:appflowy/util/field_type_extension.dart'; import 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/clear_date_button.dart'; import 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/date_type_option_button.dart'; import 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/reminder_selector.dart'; -import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart'; import 'package:appflowy/workspace/presentation/widgets/toggle/toggle.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; @@ -476,7 +475,7 @@ extension AppFlowyDatabaseTest on WidgetTester { await pumpAndSettle(); if (enter) { await testTextInput.receiveAction(TextInputAction.done); - await pumpAndSettle(); + await pumpAndSettle(const Duration(milliseconds: 500)); } else { await tapButton( find.descendant( @@ -629,12 +628,7 @@ extension AppFlowyDatabaseTest on WidgetTester { (w) => w is FieldActionCell && w.action == FieldAction.delete, ); await tapButton(deleteButton); - - final confirmButton = find.descendant( - of: find.byType(NavigatorAlertDialog), - matching: find.byType(PrimaryTextButton), - ); - await tapButton(confirmButton); + await tapButtonWithName(LocaleKeys.space_delete.tr()); } Future scrollRowDetailByOffset(Offset offset) async { @@ -788,7 +782,7 @@ extension AppFlowyDatabaseTest on WidgetTester { /// Each field has its own cell, so we can find the corresponding cell by /// the field type after create a new field. - Future findCellByFieldType(FieldType fieldType) async { + void findCellByFieldType(FieldType fieldType) { final finder = finderForFieldType(fieldType); expect(finder, findsWidgets); } @@ -894,18 +888,19 @@ extension AppFlowyDatabaseTest on WidgetTester { } Future createField( - FieldType fieldType, - String name, { + FieldType fieldType, { + String? name, ViewLayoutPB layout = ViewLayoutPB.Grid, }) async { if (layout == ViewLayoutPB.Grid) { await scrollToRight(find.byType(GridPage)); } await tapNewPropertyButton(); - await renameField(name); + if (name != null) { + await renameField(name); + } await tapSwitchFieldTypeButton(); await selectFieldType(fieldType); - await dismissFieldEditor(); } Future tapDatabaseSettingButton() async { diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/database/field/mobile_quick_field_editor.dart b/frontend/appflowy_flutter/lib/mobile/presentation/database/field/mobile_quick_field_editor.dart index 399f7632fd..311794aa42 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/database/field/mobile_quick_field_editor.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/database/field/mobile_quick_field_editor.dart @@ -62,7 +62,7 @@ class _QuickEditFieldState extends State { create: (_) => FieldEditorBloc( viewId: widget.viewId, fieldController: widget.fieldController, - field: widget.fieldInfo.field, + fieldInfo: widget.fieldInfo, isNew: false, ), child: BlocConsumer( diff --git a/frontend/appflowy_flutter/lib/plugins/database/application/field/field_editor_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database/application/field/field_editor_bloc.dart index a4748c0143..6f87403604 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/application/field/field_editor_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/application/field/field_editor_bloc.dart @@ -18,32 +18,33 @@ part 'field_editor_bloc.freezed.dart'; class FieldEditorBloc extends Bloc { FieldEditorBloc({ required this.viewId, + required this.fieldInfo, required this.fieldController, this.onFieldInserted, - required FieldPB field, required this.isNew, - }) : fieldId = field.id, - fieldService = FieldBackendService( + }) : _fieldService = FieldBackendService( viewId: viewId, - fieldId: field.id, + fieldId: fieldInfo.id, ), fieldSettingsService = FieldSettingsBackendService(viewId: viewId), - super(FieldEditorState(field: FieldInfo.initial(field))) { + super(FieldEditorState(field: fieldInfo)) { _dispatch(); _startListening(); _init(); } final String viewId; - final String fieldId; + final FieldInfo fieldInfo; final bool isNew; final FieldController fieldController; - final FieldBackendService fieldService; + final FieldBackendService _fieldService; final FieldSettingsBackendService fieldSettingsService; final void Function(String newFieldId)? onFieldInserted; late final OnReceiveField _listener; + String get fieldId => fieldInfo.id; + @override Future close() { fieldController.removeSingleFieldListener( @@ -66,13 +67,13 @@ class FieldEditorBloc extends Bloc { fieldName = fieldType.i18n; } - await fieldService.updateType( + await _fieldService.updateType( fieldType: fieldType, fieldName: fieldName, ); }, renameField: (newName) async { - final result = await fieldService.updateField(name: newName); + final result = await _fieldService.updateField(name: newName); _logIfError(result); emit(state.copyWith(wasRenameManually: true)); }, @@ -85,14 +86,14 @@ class FieldEditorBloc extends Bloc { _logIfError(result); }, insertLeft: () async { - final result = await fieldService.createBefore(); + final result = await _fieldService.createBefore(); result.fold( (newField) => onFieldInserted?.call(newField.id), (err) => Log.error("Failed creating field $err"), ); }, insertRight: () async { - final result = await fieldService.createAfter(); + final result = await _fieldService.createAfter(); result.fold( (newField) => onFieldInserted?.call(newField.id), (err) => Log.error("Failed creating field $err"), @@ -106,7 +107,7 @@ class FieldEditorBloc extends Bloc { ? FieldVisibility.AlwaysShown : FieldVisibility.AlwaysHidden; final result = await fieldSettingsService.updateFieldSettings( - fieldId: state.field.id, + fieldId: fieldId, fieldVisibility: newVisibility, ); _logIfError(result); diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/application/row/row_detail_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/application/row/row_detail_bloc.dart index bf5aff3a4a..3fcaae3332 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/application/row/row_detail_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/grid/application/row/row_detail_bloc.dart @@ -82,6 +82,15 @@ class RowDetailBloc extends Bloc { ), ); }, + startEditingField: (fieldId) { + emit(state.copyWith(editingFieldId: fieldId)); + }, + startEditingNewField: (fieldId) { + emit(state.copyWith(editingFieldId: fieldId, newFieldId: fieldId)); + }, + endEditingField: () { + emit(state.copyWith(editingFieldId: "", newFieldId: "")); + }, ); }, ); @@ -219,6 +228,16 @@ class RowDetailEvent with _$RowDetailEvent { /// Used to hide/show the hidden fields in the row detail page const factory RowDetailEvent.toggleHiddenFieldVisibility() = _ToggleHiddenFieldVisibility; + + /// Begin editing an event; + const factory RowDetailEvent.startEditingField(String fieldId) = + _StartEditingField; + + const factory RowDetailEvent.startEditingNewField(String fieldId) = + _StartEditingNewField; + + /// End editing an event + const factory RowDetailEvent.endEditingField() = _EndEditingField; } @freezed @@ -228,6 +247,8 @@ class RowDetailState with _$RowDetailState { required List visibleCells, required bool showHiddenFields, required int numHiddenFields, + required String editingFieldId, + required String newFieldId, }) = _RowDetailState; factory RowDetailState.initial() => const RowDetailState( @@ -235,5 +256,7 @@ class RowDetailState with _$RowDetailState { visibleCells: [], showHiddenFields: false, numHiddenFields: 0, + editingFieldId: "", + newFieldId: "", ); } diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/header/desktop_field_cell.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/header/desktop_field_cell.dart index b14643381d..6c1809d691 100755 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/header/desktop_field_cell.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/header/desktop_field_cell.dart @@ -86,7 +86,7 @@ class _GridFieldCellState extends State { return FieldEditor( viewId: widget.viewId, fieldController: widget.fieldController, - field: widget.fieldInfo.field, + fieldInfo: widget.fieldInfo, isNewField: widget.isNew, initialPage: widget.isNew ? FieldEditorPage.details diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/header/grid_header.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/header/grid_header.dart index bf0bf78e08..97532bcf3d 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/header/grid_header.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/header/grid_header.dart @@ -174,7 +174,7 @@ class _CellTrailing extends StatelessWidget { } } -class CreateFieldButton extends StatefulWidget { +class CreateFieldButton extends StatelessWidget { const CreateFieldButton({ super.key, required this.viewId, @@ -184,11 +184,6 @@ class CreateFieldButton extends StatefulWidget { final String viewId; final void Function(String fieldId) onFieldCreated; - @override - State createState() => _CreateFieldButtonState(); -} - -class _CreateFieldButtonState extends State { @override Widget build(BuildContext context) { return FlowyButton( @@ -202,10 +197,10 @@ class _CreateFieldButtonState extends State { hoverColor: AFThemeExtension.of(context).greyHover, onTap: () async { final result = await FieldBackendService.createField( - viewId: widget.viewId, + viewId: viewId, ); result.fold( - (field) => widget.onFieldCreated(field.id), + (field) => onFieldCreated(field.id), (err) => Log.error("Failed to create field type option: $err"), ); }, diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell_editor/checklist_cell_editor.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell_editor/checklist_cell_editor.dart index 15fe9b59ff..e86a2c47c7 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell_editor/checklist_cell_editor.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell_editor/checklist_cell_editor.dart @@ -174,6 +174,8 @@ class _ChecklistItemState extends State { meta: Platform.isMacOS, control: !Platform.isMacOS, ): const _SelectTaskIntent(), + const SingleActivator(LogicalKeyboardKey.enter): + const _EndEditingTaskIntent(), const SingleActivator(LogicalKeyboardKey.escape): const _EndEditingTaskIntent(), }; diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell_editor/mobile_checklist_cell_editor.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell_editor/mobile_checklist_cell_editor.dart index d1868d70ff..b8054bb7a7 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell_editor/mobile_checklist_cell_editor.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell_editor/mobile_checklist_cell_editor.dart @@ -138,7 +138,7 @@ class _ChecklistItemState extends State<_ChecklistItem> { Widget build(BuildContext context) { return Container( padding: const EdgeInsets.symmetric(horizontal: 5), - height: 44, + constraints: const BoxConstraints(minHeight: 44), child: Row( children: [ InkWell( @@ -164,6 +164,8 @@ class _ChecklistItemState extends State<_ChecklistItem> { controller: _textController, focusNode: _focusNode, style: Theme.of(context).textTheme.bodyMedium, + keyboardType: TextInputType.multiline, + maxLines: null, decoration: InputDecoration( border: InputBorder.none, enabledBorder: InputBorder.none, diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell_editor/relation_cell_editor.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell_editor/relation_cell_editor.dart index 63ea44008e..599ddeb017 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell_editor/relation_cell_editor.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell_editor/relation_cell_editor.dart @@ -2,8 +2,15 @@ import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/database/application/field/type_option/relation_type_option_cubit.dart'; import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart'; import 'package:appflowy/plugins/database/grid/presentation/widgets/common/type_option_separator.dart'; +import 'package:appflowy/plugins/database/tab_bar/tab_bar_view.dart'; import 'package:appflowy/plugins/database/widgets/row/relation_row_detail.dart'; +import 'package:appflowy/startup/startup.dart'; +import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart'; +import 'package:appflowy/workspace/application/view/view_ext.dart'; +import 'package:appflowy_backend/dispatch/dispatch.dart'; +import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart'; import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:collection/collection.dart'; import 'package:easy_localization/easy_localization.dart'; @@ -126,7 +133,7 @@ class _RelationCellEditorContentState shrinkWrap: true, slivers: [ _CellEditorTitle( - databaseName: widget.relatedDatabaseMeta.databaseName, + databaseMeta: widget.relatedDatabaseMeta, ), _SearchField( focusNode: focusNode, @@ -204,10 +211,10 @@ class _RelationCellEditorContentState class _CellEditorTitle extends StatelessWidget { const _CellEditorTitle({ - required this.databaseName, + required this.databaseMeta, }); - final String databaseName; + final DatabaseMeta databaseMeta; @override Widget build(BuildContext context) { @@ -223,15 +230,20 @@ class _CellEditorTitle extends StatelessWidget { fontSize: 11, color: Theme.of(context).hintColor, ), - Padding( - padding: const EdgeInsets.symmetric( - horizontal: 4, - vertical: 2, - ), - child: FlowyText.regular( - databaseName, - fontSize: 11, - overflow: TextOverflow.ellipsis, + MouseRegion( + cursor: SystemMouseCursors.click, + child: GestureDetector( + onTap: () => _openRelatedDatbase(context), + child: Padding( + padding: + const EdgeInsets.symmetric(horizontal: 4, vertical: 2), + child: FlowyText.regular( + databaseMeta.databaseName, + fontSize: 11, + overflow: TextOverflow.ellipsis, + decoration: TextDecoration.underline, + ), + ), ), ), ], @@ -239,6 +251,28 @@ class _CellEditorTitle extends StatelessWidget { ), ); } + + void _openRelatedDatbase(BuildContext context) { + FolderEventGetView(ViewIdPB(value: databaseMeta.inlineViewId)) + .send() + .then((result) { + result.fold( + (view) { + PopoverContainer.of(context).closeAll(); + Navigator.of(context).maybePop(); + getIt().add( + TabsEvent.openPlugin( + plugin: DatabaseTabBarViewPlugin( + view: view, + pluginType: view.pluginType, + ), + ), + ); + }, + (err) => Log.error(err), + ); + }); + } } class _SearchField extends StatelessWidget { diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/field/field_editor.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/field/field_editor.dart index e0d040ea64..dbcf4b8cb0 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/field/field_editor.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/field/field_editor.dart @@ -31,7 +31,7 @@ class FieldEditor extends StatefulWidget { const FieldEditor({ super.key, required this.viewId, - required this.field, + required this.fieldInfo, required this.fieldController, required this.isNewField, this.initialPage = FieldEditorPage.details, @@ -39,7 +39,7 @@ class FieldEditor extends StatefulWidget { }); final String viewId; - final FieldPB field; + final FieldInfo fieldInfo; final FieldController fieldController; final FieldEditorPage initialPage; final void Function(String fieldId)? onFieldInserted; @@ -51,13 +51,13 @@ class FieldEditor extends StatefulWidget { class _FieldEditorState extends State { late FieldEditorPage _currentPage; - late final TextEditingController textController; + late final TextEditingController textController = + TextEditingController(text: widget.fieldInfo.name); @override void initState() { super.initState(); _currentPage = widget.initialPage; - textController = TextEditingController(text: widget.field.name); } @override @@ -71,14 +71,14 @@ class _FieldEditorState extends State { return BlocProvider( create: (_) => FieldEditorBloc( viewId: widget.viewId, - field: widget.field, + fieldInfo: widget.fieldInfo, fieldController: widget.fieldController, onFieldInserted: widget.onFieldInserted, isNew: widget.isNewField, ), - child: _currentPage == FieldEditorPage.details - ? _fieldDetails() - : _fieldGeneral(), + child: _currentPage == FieldEditorPage.general + ? _fieldGeneral() + : _fieldDetails(), ); } @@ -120,6 +120,21 @@ class _FieldEditorState extends State { ); } + Widget _actionCell(FieldAction action) { + return BlocBuilder( + builder: (context, state) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: FieldActionCell( + viewId: widget.viewId, + fieldInfo: state.field, + action: action, + ), + ); + }, + ); + } + Widget _fieldDetails() { return SizedBox( width: 260, @@ -129,19 +144,6 @@ class _FieldEditorState extends State { ), ); } - - Widget _actionCell(FieldAction action) { - return BlocBuilder( - builder: (context, state) => Padding( - padding: const EdgeInsets.symmetric(horizontal: 8.0), - child: FieldActionCell( - viewId: widget.viewId, - fieldInfo: state.field, - action: action, - ), - ), - ); - } } class _EditFieldButton extends StatelessWidget { @@ -322,32 +324,33 @@ enum FieldAction { ); break; case FieldAction.clearData: - NavigatorAlertDialog( - constraints: const BoxConstraints( - maxWidth: 250, - maxHeight: 260, - ), - title: LocaleKeys.grid_field_clearFieldPromptMessage.tr(), - confirm: () { + PopoverContainer.of(context).closeAll(); + showCancelAndConfirmDialog( + context: context, + title: LocaleKeys.grid_field_label.tr(), + description: LocaleKeys.grid_field_clearFieldPromptMessage.tr(), + confirmLabel: LocaleKeys.button_confirm.tr(), + onConfirm: () { FieldBackendService.clearField( viewId: viewId, fieldId: fieldInfo.id, ); }, - ).show(context); - PopoverContainer.of(context).close(); + ); break; case FieldAction.delete: - NavigatorAlertDialog( - title: LocaleKeys.grid_field_deleteFieldPromptMessage.tr(), - confirm: () { + PopoverContainer.of(context).closeAll(); + showConfirmDeletionDialog( + context: context, + name: LocaleKeys.grid_field_label.tr(), + description: LocaleKeys.grid_field_deleteFieldPromptMessage.tr(), + onConfirm: () { FieldBackendService.deleteField( viewId: viewId, fieldId: fieldInfo.id, ); }, - ).show(context); - PopoverContainer.of(context).close(); + ); break; case FieldAction.wrap: context @@ -574,7 +577,10 @@ class _FieldNameTextFieldState extends State { } class SwitchFieldButton extends StatefulWidget { - const SwitchFieldButton({super.key, required this.popoverMutex}); + const SwitchFieldButton({ + super.key, + required this.popoverMutex, + }); final PopoverMutex popoverMutex; diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/row/row_property.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/row/row_property.dart index 304e444761..300e84961c 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/row/row_property.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/row/row_property.dart @@ -14,7 +14,6 @@ import 'package:appflowy/plugins/database/grid/presentation/widgets/header/deskt import 'package:appflowy/plugins/database/widgets/field/field_editor.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/block_action_button.dart'; import 'package:appflowy_backend/log.dart'; -import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart'; import 'package:appflowy_editor/appflowy_editor.dart' hide Log; import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:collection/collection.dart'; @@ -130,50 +129,11 @@ class _PropertyCell extends StatefulWidget { class _PropertyCellState extends State<_PropertyCell> { final PopoverController _popoverController = PopoverController(); - final PopoverController _fieldPopoverController = PopoverController(); final ValueNotifier _isFieldHover = ValueNotifier(false); @override Widget build(BuildContext context) { - final dragThumb = MouseRegion( - cursor: Platform.isWindows - ? SystemMouseCursors.click - : SystemMouseCursors.grab, - child: SizedBox( - width: 16, - height: 30, - child: AppFlowyPopover( - controller: _fieldPopoverController, - constraints: BoxConstraints.loose(const Size(240, 600)), - margin: EdgeInsets.zero, - triggerActions: PopoverTriggerFlags.none, - direction: PopoverDirection.bottomWithLeftAligned, - popupBuilder: (popoverContext) => FieldEditor( - viewId: widget.fieldController.viewId, - field: widget.fieldController - .getField(widget.cellContext.fieldId)! - .field, - fieldController: widget.fieldController, - isNewField: false, - ), - child: ValueListenableBuilder( - valueListenable: _isFieldHover, - builder: (_, isHovering, child) => - isHovering ? child! : const SizedBox.shrink(), - child: BlockActionButton( - onTap: () => _fieldPopoverController.show(), - svg: FlowySvgs.drag_element_s, - richMessage: TextSpan( - text: LocaleKeys.grid_rowPage_fieldDragElementTooltip.tr(), - style: context.tooltipTextStyle(), - ), - ), - ), - ), - ), - ); - final cell = widget.cellBuilder.buildStyled( widget.cellContext, EditableCellStyle.desktopRowDetail, @@ -210,53 +170,12 @@ class _PropertyCellState extends State<_PropertyCell> { return ReorderableDragStartListener( index: widget.index, enabled: value, - child: dragThumb, + child: _buildDragHandle(context), ); }, ), const HSpace(4), - BlocSelector( - selector: (state) => state.fields.firstWhereOrNull( - (fieldInfo) => fieldInfo.field.id == widget.cellContext.fieldId, - ), - builder: (context, fieldInfo) { - if (fieldInfo == null) { - return const SizedBox.shrink(); - } - return AppFlowyPopover( - controller: _popoverController, - constraints: BoxConstraints.loose(const Size(240, 600)), - margin: EdgeInsets.zero, - triggerActions: PopoverTriggerFlags.none, - direction: PopoverDirection.bottomWithLeftAligned, - popupBuilder: (popoverContext) => FieldEditor( - viewId: widget.fieldController.viewId, - field: fieldInfo.field, - fieldController: widget.fieldController, - isNewField: false, - ), - child: SizedBox( - width: 160, - height: 30, - child: Tooltip( - waitDuration: const Duration(seconds: 1), - preferBelow: false, - verticalOffset: 15, - message: fieldInfo.name, - child: FieldCellButton( - field: fieldInfo.field, - onTap: () => _popoverController.show(), - radius: BorderRadius.circular(6), - margin: const EdgeInsets.symmetric( - horizontal: 4, - vertical: 6, - ), - ), - ), - ), - ); - }, - ), + _buildFieldButton(context), const HSpace(8), Expanded(child: gesture), ], @@ -264,6 +183,96 @@ class _PropertyCellState extends State<_PropertyCell> { ), ); } + + Widget _buildDragHandle(BuildContext context) { + return MouseRegion( + cursor: Platform.isWindows + ? SystemMouseCursors.click + : SystemMouseCursors.grab, + child: SizedBox( + width: 16, + height: 30, + child: BlocListener( + listenWhen: (previous, current) => + previous.editingFieldId != current.editingFieldId, + listener: (context, state) { + if (state.editingFieldId == widget.cellContext.fieldId) { + WidgetsBinding.instance.addPostFrameCallback((_) { + _popoverController.show(); + }); + } + }, + child: ValueListenableBuilder( + valueListenable: _isFieldHover, + builder: (_, isHovering, child) => + isHovering ? child! : const SizedBox.shrink(), + child: BlockActionButton( + onTap: () => context.read().add( + RowDetailEvent.startEditingField( + widget.cellContext.fieldId, + ), + ), + svg: FlowySvgs.drag_element_s, + richMessage: TextSpan( + text: LocaleKeys.grid_rowPage_fieldDragElementTooltip.tr(), + style: context.tooltipTextStyle(), + ), + ), + ), + ), + ), + ); + } + + Widget _buildFieldButton(BuildContext context) { + return BlocSelector( + selector: (state) => state.fields.firstWhereOrNull( + (fieldInfo) => fieldInfo.field.id == widget.cellContext.fieldId, + ), + builder: (context, fieldInfo) { + if (fieldInfo == null) { + return const SizedBox.shrink(); + } + return AppFlowyPopover( + controller: _popoverController, + constraints: BoxConstraints.loose(const Size(240, 600)), + margin: EdgeInsets.zero, + triggerActions: PopoverTriggerFlags.none, + direction: PopoverDirection.bottomWithLeftAligned, + onClose: () => context + .read() + .add(const RowDetailEvent.endEditingField()), + popupBuilder: (popoverContext) => FieldEditor( + viewId: widget.fieldController.viewId, + fieldInfo: fieldInfo, + fieldController: widget.fieldController, + isNewField: context.watch().state.newFieldId == + widget.cellContext.fieldId, + ), + child: SizedBox( + width: 160, + height: 30, + child: Tooltip( + waitDuration: const Duration(seconds: 1), + preferBelow: false, + verticalOffset: 15, + message: fieldInfo.name, + child: FieldCellButton( + field: fieldInfo.field, + onTap: () => context.read().add( + RowDetailEvent.startEditingField( + widget.cellContext.fieldId, + ), + ), + radius: BorderRadius.circular(6), + margin: const EdgeInsets.symmetric(horizontal: 4, vertical: 6), + ), + ), + ), + ); + }, + ); + } } class ToggleHiddenFieldsVisibilityButton extends StatelessWidget { @@ -357,7 +366,7 @@ class ToggleHiddenFieldsVisibilityButton extends StatelessWidget { } } -class CreateRowFieldButton extends StatefulWidget { +class CreateRowFieldButton extends StatelessWidget { const CreateRowFieldButton({ super.key, required this.viewId, @@ -367,61 +376,35 @@ class CreateRowFieldButton extends StatefulWidget { final String viewId; final FieldController fieldController; - @override - State createState() => _CreateRowFieldButtonState(); -} - -class _CreateRowFieldButtonState extends State { - final PopoverController popoverController = PopoverController(); - FieldPB? createdField; - @override Widget build(BuildContext context) { - return AppFlowyPopover( - constraints: BoxConstraints.loose(const Size(240, 200)), - controller: popoverController, - direction: PopoverDirection.topWithLeftAligned, - triggerActions: PopoverTriggerFlags.none, - margin: EdgeInsets.zero, - child: SizedBox( - height: 30, - child: FlowyButton( - margin: const EdgeInsets.symmetric(horizontal: 4, vertical: 6), - text: FlowyText.medium( - lineHeight: 1.0, - LocaleKeys.grid_field_newProperty.tr(), - color: Theme.of(context).hintColor, - ), - hoverColor: AFThemeExtension.of(context).lightGreyHover, - onTap: () async { - final result = await FieldBackendService.createField( - viewId: widget.viewId, - ); - result.fold( - (newField) { - createdField = newField; - popoverController.show(); - }, - (r) => Log.error("Failed to create field type option: $r"), - ); - }, - leftIcon: FlowySvg( - FlowySvgs.add_m, - color: Theme.of(context).hintColor, - ), + return SizedBox( + height: 30, + child: FlowyButton( + margin: const EdgeInsets.symmetric(horizontal: 4, vertical: 6), + text: FlowyText.medium( + lineHeight: 1.0, + LocaleKeys.grid_field_newProperty.tr(), + color: Theme.of(context).hintColor, + ), + hoverColor: AFThemeExtension.of(context).lightGreyHover, + onTap: () async { + final result = await FieldBackendService.createField( + viewId: viewId, + ); + await Future.delayed(const Duration(milliseconds: 50)); + result.fold( + (field) => context + .read() + .add(RowDetailEvent.startEditingNewField(field.id)), + (err) => Log.error("Failed to create field type option: $err"), + ); + }, + leftIcon: FlowySvg( + FlowySvgs.add_m, + color: Theme.of(context).hintColor, ), ), - popupBuilder: (BuildContext popoverContext) { - if (createdField == null) { - return const SizedBox.shrink(); - } - return FieldEditor( - viewId: widget.viewId, - field: createdField!, - fieldController: widget.fieldController, - isNewField: true, - ); - }, ); } } diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/setting/setting_property_list.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/setting/setting_property_list.dart index 125e0b08f9..87c7002e09 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/setting/setting_property_list.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/setting/setting_property_list.dart @@ -204,7 +204,7 @@ class _DatabasePropertyCellState extends State { popupBuilder: (BuildContext context) { return FieldEditor( viewId: widget.viewId, - field: widget.fieldInfo.field, + fieldInfo: widget.fieldInfo, fieldController: widget.fieldController, isNewField: false, ); diff --git a/frontend/appflowy_flutter/pubspec.lock b/frontend/appflowy_flutter/pubspec.lock index 54a1cbee29..fc1f023a98 100644 --- a/frontend/appflowy_flutter/pubspec.lock +++ b/frontend/appflowy_flutter/pubspec.lock @@ -133,10 +133,10 @@ packages: dependency: transitive description: name: bidi - sha256: "1a7d0c696324b2089f72e7671fd1f1f64fef44c980f3cebc84e803967c597b63" + sha256: "9a712c7ddf708f7c41b1923aa83648a3ed44cfd75b04f72d598c45e5be287f9d" url: "https://pub.dev" source: hosted - version: "2.0.10" + version: "2.0.12" bitsdojo_window: dependency: "direct main" description: diff --git a/frontend/appflowy_flutter/test/bloc_test/board_test/create_or_edit_field_test.dart b/frontend/appflowy_flutter/test/bloc_test/board_test/create_or_edit_field_test.dart index 187fa6d5e9..9131446347 100644 --- a/frontend/appflowy_flutter/test/bloc_test/board_test/create_or_edit_field_test.dart +++ b/frontend/appflowy_flutter/test/bloc_test/board_test/create_or_edit_field_test.dart @@ -34,7 +34,7 @@ void main() { final editorBloc = FieldEditorBloc( viewId: context.gridView.id, - field: fieldInfo.field, + fieldInfo: fieldInfo, fieldController: context.fieldController, isNew: false, ); diff --git a/frontend/appflowy_flutter/test/bloc_test/board_test/util.dart b/frontend/appflowy_flutter/test/bloc_test/board_test/util.dart index e89f5a8815..01b5c0d5fc 100644 --- a/frontend/appflowy_flutter/test/bloc_test/board_test/util.dart +++ b/frontend/appflowy_flutter/test/bloc_test/board_test/util.dart @@ -78,15 +78,13 @@ class BoardTestContext { FieldEditorBloc makeFieldEditor({ required FieldInfo fieldInfo, - }) { - final editorBloc = FieldEditorBloc( - viewId: databaseController.viewId, - fieldController: fieldController, - field: fieldInfo.field, - isNew: false, - ); - return editorBloc; - } + }) => + FieldEditorBloc( + viewId: databaseController.viewId, + fieldController: fieldController, + fieldInfo: fieldInfo, + isNew: false, + ); CellController makeCellControllerFromFieldId(String fieldId) { return makeCellController( diff --git a/frontend/appflowy_flutter/test/bloc_test/grid_test/field/edit_field_test.dart b/frontend/appflowy_flutter/test/bloc_test/grid_test/field/edit_field_test.dart index b3a3a3b5cf..64113fc550 100644 --- a/frontend/appflowy_flutter/test/bloc_test/grid_test/field/edit_field_test.dart +++ b/frontend/appflowy_flutter/test/bloc_test/grid_test/field/edit_field_test.dart @@ -10,7 +10,7 @@ Future createEditorBloc(AppFlowyGridTest gridTest) async { return FieldEditorBloc( viewId: context.gridView.id, fieldController: context.fieldController, - field: fieldInfo.field, + fieldInfo: fieldInfo, isNew: false, ); } diff --git a/frontend/appflowy_flutter/test/bloc_test/grid_test/util.dart b/frontend/appflowy_flutter/test/bloc_test/grid_test/util.dart index c652bfcb6a..28c267f570 100644 --- a/frontend/appflowy_flutter/test/bloc_test/grid_test/util.dart +++ b/frontend/appflowy_flutter/test/bloc_test/grid_test/util.dart @@ -102,7 +102,7 @@ Future createFieldEditor({ return FieldEditorBloc( viewId: databaseController.viewId, fieldController: databaseController.fieldController, - field: field, + fieldInfo: databaseController.fieldController.getField(field.id)!, isNew: true, ); }, diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index 3ab4686b4f..471bdb14b0 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -1298,6 +1298,7 @@ "isNotEmpty": "Is not empty" }, "field": { + "label": "Property", "hide": "Hide", "show": "Show", "insertLeft": "Insert Left", @@ -1351,7 +1352,7 @@ "editProperty": "Edit property", "newProperty": "New property", "openRowDocument": "Open as a page", - "deleteFieldPromptMessage": "Are you sure? This property will be deleted", + "deleteFieldPromptMessage": "Are you sure? This property and all its data will be deleted", "clearFieldPromptMessage": "Are you sure? All cells in this column will be emptied", "newColumn": "New Column", "format": "Format", diff --git a/frontend/scripts/code_generation/freezed/generate_freezed.sh b/frontend/scripts/code_generation/freezed/generate_freezed.sh index fb7dbffb20..45de7573d7 100755 --- a/frontend/scripts/code_generation/freezed/generate_freezed.sh +++ b/frontend/scripts/code_generation/freezed/generate_freezed.sh @@ -110,7 +110,7 @@ fi if [ "$verbose" = true ]; then dart run build_runner build -d & else - dart run build_runner build >/dev/null 2>&1 & + dart run build_runner build -d >/dev/null 2>&1 & fi # Get the PID of the background process