fix: various grid ui issues (#6182)

* fix: delete field confirmation dialog only closes top most popover

* fix: prioritize single-line checklist items

* chore: wrap text toggle persist

* test: update integration tests

* chore: delete conflicting outputs on freezed

* chore: slightly make field editor faster

* chore: use standard dialog componet

* chore: enable multiline checklist tasks on mobile

* chore: Update frontend/appflowy_flutter/lib/plugins/database/application/field/field_editor_bloc.dart

Co-authored-by: Mathias Mogensen <42929161+Xazin@users.noreply.github.com>

* chore: code cleanup

* fix: create field from row detail and add test

* chore: allow opening related database from editor

* test: integration test flake

---------

Co-authored-by: Mathias Mogensen <42929161+Xazin@users.noreply.github.com>
This commit is contained in:
Richard Shiue 2024-09-05 13:54:50 +08:00 committed by GitHub
parent aa621289e9
commit 0fd0483302
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
23 changed files with 325 additions and 307 deletions

View File

@ -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);

View File

@ -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);

View File

@ -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);

View File

@ -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));
}
});

View File

@ -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<void> 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<void> findCellByFieldType(FieldType fieldType) async {
void findCellByFieldType(FieldType fieldType) {
final finder = finderForFieldType(fieldType);
expect(finder, findsWidgets);
}
@ -894,18 +888,19 @@ extension AppFlowyDatabaseTest on WidgetTester {
}
Future<void> 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<void> tapDatabaseSettingButton() async {

View File

@ -62,7 +62,7 @@ class _QuickEditFieldState extends State<QuickEditField> {
create: (_) => FieldEditorBloc(
viewId: widget.viewId,
fieldController: widget.fieldController,
field: widget.fieldInfo.field,
fieldInfo: widget.fieldInfo,
isNew: false,
),
child: BlocConsumer<FieldEditorBloc, FieldEditorState>(

View File

@ -18,32 +18,33 @@ part 'field_editor_bloc.freezed.dart';
class FieldEditorBloc extends Bloc<FieldEditorEvent, FieldEditorState> {
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<void> close() {
fieldController.removeSingleFieldListener(
@ -66,13 +67,13 @@ class FieldEditorBloc extends Bloc<FieldEditorEvent, FieldEditorState> {
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<FieldEditorEvent, FieldEditorState> {
_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<FieldEditorEvent, FieldEditorState> {
? FieldVisibility.AlwaysShown
: FieldVisibility.AlwaysHidden;
final result = await fieldSettingsService.updateFieldSettings(
fieldId: state.field.id,
fieldId: fieldId,
fieldVisibility: newVisibility,
);
_logIfError(result);

View File

@ -82,6 +82,15 @@ class RowDetailBloc extends Bloc<RowDetailEvent, RowDetailState> {
),
);
},
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<CellContext> 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: "",
);
}

View File

@ -86,7 +86,7 @@ class _GridFieldCellState extends State<GridFieldCell> {
return FieldEditor(
viewId: widget.viewId,
fieldController: widget.fieldController,
field: widget.fieldInfo.field,
fieldInfo: widget.fieldInfo,
isNewField: widget.isNew,
initialPage: widget.isNew
? FieldEditorPage.details

View File

@ -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<CreateFieldButton> createState() => _CreateFieldButtonState();
}
class _CreateFieldButtonState extends State<CreateFieldButton> {
@override
Widget build(BuildContext context) {
return FlowyButton(
@ -202,10 +197,10 @@ class _CreateFieldButtonState extends State<CreateFieldButton> {
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"),
);
},

View File

@ -174,6 +174,8 @@ class _ChecklistItemState extends State<ChecklistItem> {
meta: Platform.isMacOS,
control: !Platform.isMacOS,
): const _SelectTaskIntent(),
const SingleActivator(LogicalKeyboardKey.enter):
const _EndEditingTaskIntent(),
const SingleActivator(LogicalKeyboardKey.escape):
const _EndEditingTaskIntent(),
};

View File

@ -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,

View File

@ -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<TabsBloc>().add(
TabsEvent.openPlugin(
plugin: DatabaseTabBarViewPlugin(
view: view,
pluginType: view.pluginType,
),
),
);
},
(err) => Log.error(err),
);
});
}
}
class _SearchField extends StatelessWidget {

View File

@ -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<FieldEditor> {
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<FieldEditor> {
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<FieldEditor> {
);
}
Widget _actionCell(FieldAction action) {
return BlocBuilder<FieldEditorBloc, FieldEditorState>(
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<FieldEditor> {
),
);
}
Widget _actionCell(FieldAction action) {
return BlocBuilder<FieldEditorBloc, FieldEditorState>(
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<FieldNameTextField> {
}
class SwitchFieldButton extends StatefulWidget {
const SwitchFieldButton({super.key, required this.popoverMutex});
const SwitchFieldButton({
super.key,
required this.popoverMutex,
});
final PopoverMutex popoverMutex;

View File

@ -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<bool> _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<RowDetailBloc, RowDetailState, FieldInfo?>(
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<RowDetailBloc, RowDetailState>(
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<RowDetailBloc>().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<RowDetailBloc, RowDetailState, FieldInfo?>(
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<RowDetailBloc>()
.add(const RowDetailEvent.endEditingField()),
popupBuilder: (popoverContext) => FieldEditor(
viewId: widget.fieldController.viewId,
fieldInfo: fieldInfo,
fieldController: widget.fieldController,
isNewField: context.watch<RowDetailBloc>().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<RowDetailBloc>().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<CreateRowFieldButton> createState() => _CreateRowFieldButtonState();
}
class _CreateRowFieldButtonState extends State<CreateRowFieldButton> {
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<RowDetailBloc>()
.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,
);
},
);
}
}

View File

@ -204,7 +204,7 @@ class _DatabasePropertyCellState extends State<DatabasePropertyCell> {
popupBuilder: (BuildContext context) {
return FieldEditor(
viewId: widget.viewId,
field: widget.fieldInfo.field,
fieldInfo: widget.fieldInfo,
fieldController: widget.fieldController,
isNewField: false,
);

View File

@ -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:

View File

@ -34,7 +34,7 @@ void main() {
final editorBloc = FieldEditorBloc(
viewId: context.gridView.id,
field: fieldInfo.field,
fieldInfo: fieldInfo,
fieldController: context.fieldController,
isNew: false,
);

View File

@ -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(

View File

@ -10,7 +10,7 @@ Future<FieldEditorBloc> createEditorBloc(AppFlowyGridTest gridTest) async {
return FieldEditorBloc(
viewId: context.gridView.id,
fieldController: context.fieldController,
field: fieldInfo.field,
fieldInfo: fieldInfo,
isNew: false,
);
}

View File

@ -102,7 +102,7 @@ Future<FieldEditorBloc> createFieldEditor({
return FieldEditorBloc(
viewId: databaseController.viewId,
fieldController: databaseController.fieldController,
field: field,
fieldInfo: databaseController.fieldController.getField(field.id)!,
isNew: true,
);
},

View File

@ -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",

View File

@ -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