fix: cell focus issue on windows ()

* fix: cell focus issue on windows

* fix: try to fix

* fix: cell focus not working on Windows platform there are multiple text cells

* docs: add documentation

* chore: adjust row detail page ui

* test: add test

* test: fix test

---------

Co-authored-by: vedon <vedon.fu@gmail.com>
This commit is contained in:
Nathan.fooo 2023-06-22 20:16:31 +08:00 committed by GitHub
parent f1bfcb6066
commit a29c8ab27a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 169 additions and 92 deletions
frontend
appflowy_flutter
appflowy_tauri/src-tauri

View File

@ -1,4 +1,5 @@
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart';
import 'package:appflowy_backend/protobuf/flowy-folder2/protobuf.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:intl/intl.dart';
@ -47,6 +48,44 @@ void main() {
await tester.pumpAndSettle();
});
// Makesure the text cells are filled with the right content when there are
// multiple text cell
testWidgets('edit multiple text cells', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
await tester.createNewPageWithName(ViewLayoutPB.Grid, 'my grid');
await tester.createField(FieldType.RichText, 'description');
await tester.editCell(
rowIndex: 0,
fieldType: FieldType.RichText,
input: 'hello',
);
await tester.editCell(
rowIndex: 0,
fieldType: FieldType.RichText,
input: 'world',
cellIndex: 1,
);
await tester.assertCellContent(
rowIndex: 0,
fieldType: FieldType.RichText,
content: 'hello',
cellIndex: 0,
);
await tester.assertCellContent(
rowIndex: 0,
fieldType: FieldType.RichText,
content: 'world',
cellIndex: 1,
);
await tester.pumpAndSettle();
});
testWidgets('edit number cell', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();

View File

@ -108,22 +108,26 @@ extension AppFlowyDatabaseTest on WidgetTester {
required int rowIndex,
required FieldType fieldType,
required String input,
int cellIndex = 0,
}) async {
final cell = cellFinder(rowIndex, fieldType);
final cell = cellFinder(rowIndex, fieldType, cellIndex: cellIndex);
expect(cell, findsOneWidget);
await enterText(cell, input);
await pumpAndSettle();
}
Finder cellFinder(int rowIndex, FieldType fieldType) {
///
Finder cellFinder(int rowIndex, FieldType fieldType, {int cellIndex = 0}) {
final findRow = find.byType(GridRow, skipOffstage: false);
final findCell = finderForFieldType(fieldType);
return find.descendant(
of: findRow.at(rowIndex),
matching: findCell,
skipOffstage: false,
);
return find
.descendant(
of: findRow.at(rowIndex),
matching: findCell,
skipOffstage: false,
)
.at(cellIndex);
}
Future<void> tapCheckboxCellInGrid({
@ -173,8 +177,9 @@ extension AppFlowyDatabaseTest on WidgetTester {
required int rowIndex,
required FieldType fieldType,
required String content,
int cellIndex = 0,
}) async {
final findCell = cellFinder(rowIndex, fieldType);
final findCell = cellFinder(rowIndex, fieldType, cellIndex: cellIndex);
final findContent = find.descendant(
of: findCell,
matching: find.text(content),

View File

@ -1,3 +1,5 @@
import 'dart:io';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/database_view/application/row/row_service.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/toolbar/grid_setting_bar.dart';
@ -241,41 +243,7 @@ class _GridRows extends StatelessWidget {
);
return ScrollConfiguration(
behavior: behavior,
child: ReorderableListView.builder(
/// TODO(Xazin): Resolve inconsistent scrollbar behavior
/// This is a workaround related to
/// https://github.com/flutter/flutter/issues/25652
cacheExtent: 5000,
scrollController: scrollController.verticalController,
buildDefaultDragHandles: false,
proxyDecorator: (child, index, animation) => Material(
color: Colors.white.withOpacity(.1),
child: Opacity(opacity: .5, child: child),
),
onReorder: (fromIndex, newIndex) {
final toIndex =
newIndex > fromIndex ? newIndex - 1 : newIndex;
if (fromIndex == toIndex) {
return;
}
context
.read<GridBloc>()
.add(GridEvent.moveRow(fromIndex, toIndex));
},
itemCount: rowInfos.length + 1, // the extra item is the footer
itemBuilder: (context, index) {
if (index < rowInfos.length) {
final rowInfo = rowInfos[index];
return _renderRow(
context,
rowInfo.rowId,
isDraggable: state.reorderable,
index: index,
);
}
return const GridRowBottomBar(key: Key('gridFooter'));
},
),
child: _renderList(context, state, rowInfos),
);
},
),
@ -283,6 +251,66 @@ class _GridRows extends StatelessWidget {
);
}
Widget _renderList(
BuildContext context,
GridState state,
List<RowInfo> rowInfos,
) {
if (Platform.isWindows) {
// Workaround: On Windows, the focusing of the text cell is not working
// properly when the list is reorderable. So using the ListView instead.
return ListView.builder(
controller: scrollController.verticalController,
itemCount: rowInfos.length + 1, // the extra item is the footer
itemBuilder: (context, index) {
if (index < rowInfos.length) {
final rowInfo = rowInfos[index];
return _renderRow(
context,
rowInfo.rowId,
isDraggable: false,
index: index,
);
}
return const GridRowBottomBar(key: Key('gridFooter'));
},
);
} else {
return ReorderableListView.builder(
/// TODO(Xazin): Resolve inconsistent scrollbar behavior
/// This is a workaround related to
/// https://github.com/flutter/flutter/issues/25652
cacheExtent: 5000,
scrollController: scrollController.verticalController,
buildDefaultDragHandles: false,
proxyDecorator: (child, index, animation) => Material(
color: Colors.white.withOpacity(.1),
child: Opacity(opacity: .5, child: child),
),
onReorder: (fromIndex, newIndex) {
final toIndex = newIndex > fromIndex ? newIndex - 1 : newIndex;
if (fromIndex == toIndex) {
return;
}
context.read<GridBloc>().add(GridEvent.moveRow(fromIndex, toIndex));
},
itemCount: rowInfos.length + 1, // the extra item is the footer
itemBuilder: (context, index) {
if (index < rowInfos.length) {
final rowInfo = rowInfos[index];
return _renderRow(
context,
rowInfo.rowId,
isDraggable: state.reorderable,
index: index,
);
}
return const GridRowBottomBar(key: Key('gridFooter'));
},
);
}
}
Widget _renderRow(
BuildContext context,
RowId rowId, {

View File

@ -138,7 +138,6 @@ class _OptionNameTextField extends StatelessWidget {
return FlowyTextField(
autoFocus: autoFocus,
text: name,
maxLength: 30,
submitOnLeave: true,
onSubmitted: (newName) {
if (name != newName) {

View File

@ -103,11 +103,11 @@ class BlankCell extends StatelessWidget {
}
abstract class CellEditable {
GridCellFocusListener get beginFocus;
RequestFocusListener get requestFocus;
ValueNotifier<bool> get onCellFocus;
ValueNotifier<bool> get onCellEditing;
// ValueNotifier<bool> get onCellEditing;
}
typedef AccessoryBuilder = List<GridCellAccessoryBuilder> Function(
@ -125,11 +125,7 @@ abstract class CellAccessory extends Widget {
abstract class GridCellWidget extends StatefulWidget
implements CellAccessory, CellEditable, CellShortcuts {
GridCellWidget({Key? key}) : super(key: key) {
onCellEditing.addListener(() {
onCellFocus.value = onCellEditing.value;
});
}
GridCellWidget({super.key});
@override
final ValueNotifier<bool> onCellFocus = ValueNotifier<bool>(false);
@ -138,8 +134,8 @@ abstract class GridCellWidget extends StatefulWidget
@override
ValueNotifier<bool> get onAccessoryHover => onCellFocus;
@override
final ValueNotifier<bool> onCellEditing = ValueNotifier<bool>(false);
// @override
// final ValueNotifier<bool> onCellEditing = ValueNotifier<bool>(false);
@override
List<GridCellAccessoryBuilder> Function(
@ -147,7 +143,7 @@ abstract class GridCellWidget extends StatefulWidget
)? get accessoryBuilder => null;
@override
final GridCellFocusListener beginFocus = GridCellFocusListener();
final RequestFocusListener requestFocus = RequestFocusListener();
@override
final Map<CellKeyboardKey, CellKeyboardAction> shortcutHandlers = {};
@ -156,7 +152,7 @@ abstract class GridCellWidget extends StatefulWidget
abstract class GridCellState<T extends GridCellWidget> extends State<T> {
@override
void initState() {
widget.beginFocus.setListener(() => requestBeginFocus());
widget.requestFocus.setListener(requestBeginFocus);
widget.shortcutHandlers[CellKeyboardKey.onCopy] = () => onCopy();
widget.shortcutHandlers[CellKeyboardKey.onInsert] = () {
Clipboard.getData("text/plain").then((data) {
@ -172,17 +168,18 @@ abstract class GridCellState<T extends GridCellWidget> extends State<T> {
@override
void didUpdateWidget(covariant T oldWidget) {
if (oldWidget != this) {
widget.beginFocus.setListener(() => requestBeginFocus());
widget.requestFocus.setListener(requestBeginFocus);
}
super.didUpdateWidget(oldWidget);
}
@override
void dispose() {
widget.beginFocus.removeAllListener();
widget.requestFocus.removeAllListener();
super.dispose();
}
/// Subclass can override this method to request focus.
void requestBeginFocus();
String? onCopy() => null;
@ -190,9 +187,9 @@ abstract class GridCellState<T extends GridCellWidget> extends State<T> {
void onInsert(String value) {}
}
abstract class GridFocusNodeCellState<T extends GridCellWidget>
abstract class GridEditableTextCell<T extends GridCellWidget>
extends GridCellState<T> {
SingleListenerFocusNode focusNode = SingleListenerFocusNode();
SingleListenerFocusNode get focusNode;
@override
void initState() {
@ -226,9 +223,9 @@ abstract class GridFocusNodeCellState<T extends GridCellWidget>
}
void _listenOnFocusNodeChanged() {
widget.onCellEditing.value = focusNode.hasFocus;
widget.onCellFocus.value = focusNode.hasFocus;
focusNode.setListener(() {
widget.onCellEditing.value = focusNode.hasFocus;
widget.onCellFocus.value = focusNode.hasFocus;
focusChanged();
});
}
@ -236,7 +233,7 @@ abstract class GridFocusNodeCellState<T extends GridCellWidget>
Future<void> focusChanged() async {}
}
class GridCellFocusListener extends ChangeNotifier {
class RequestFocusListener extends ChangeNotifier {
VoidCallback? _listener;
void setListener(VoidCallback listener) {

View File

@ -51,8 +51,8 @@ class CellContainer extends StatelessWidget {
}
return GestureDetector(
behavior: HitTestBehavior.translucent,
onTap: () => child.beginFocus.notify(),
behavior: HitTestBehavior.opaque,
onTap: () => child.requestFocus.notify(),
child: Container(
constraints: BoxConstraints(maxWidth: width, minHeight: 46),
decoration: _makeBoxDecoration(context, isFocus),

View File

@ -45,14 +45,14 @@ class GridChecklistCellState extends GridCellState<GridChecklistCell> {
triggerActions: PopoverTriggerFlags.none,
popupBuilder: (BuildContext context) {
WidgetsBinding.instance.addPostFrameCallback((_) {
widget.onCellEditing.value = true;
widget.onCellFocus.value = true;
});
return GridChecklistCellEditor(
cellController:
widget.cellControllerBuilder.build() as ChecklistCellController,
);
},
onClose: () => widget.onCellEditing.value = false,
onClose: () => widget.onCellFocus.value = false,
child: Padding(
padding: GridSize.cellContentInsets,
child: BlocBuilder<ChecklistCardCellBloc, ChecklistCellState>(

View File

@ -90,11 +90,11 @@ class _DateCellState extends GridCellState<GridDateCell> {
return DateCellEditor(
cellController: widget.cellControllerBuilder.build()
as DateCellController,
onDismissed: () => widget.onCellEditing.value = false,
onDismissed: () => widget.onCellFocus.value = false,
);
},
onClose: () {
widget.onCellEditing.value = false;
widget.onCellFocus.value = false;
},
);
}
@ -115,7 +115,7 @@ class _DateCellState extends GridCellState<GridDateCell> {
_popover.show();
if (widget.editable) {
widget.onCellEditing.value = true;
widget.onCellFocus.value = true;
}
}

View File

@ -16,13 +16,16 @@ class GridNumberCell extends GridCellWidget {
}) : super(key: key);
@override
GridFocusNodeCellState<GridNumberCell> createState() => _NumberCellState();
GridEditableTextCell<GridNumberCell> createState() => _NumberCellState();
}
class _NumberCellState extends GridFocusNodeCellState<GridNumberCell> {
class _NumberCellState extends GridEditableTextCell<GridNumberCell> {
late NumberCellBloc _cellBloc;
late TextEditingController _controller;
@override
SingleListenerFocusNode focusNode = SingleListenerFocusNode();
@override
void initState() {
final cellController =

View File

@ -62,7 +62,7 @@ class _SingleSelectCellState extends GridCellState<GridSingleSelectCell> {
return SelectOptionWrap(
selectOptions: state.selectedOptions,
cellStyle: widget.cellStyle,
onCellEditing: widget.onCellEditing,
onCellEditing: widget.onCellFocus,
popoverController: _popover,
cellControllerBuilder: widget.cellControllerBuilder,
);
@ -125,7 +125,7 @@ class _MultiSelectCellState extends GridCellState<GridMultiSelectCell> {
return SelectOptionWrap(
selectOptions: state.selectedOptions,
cellStyle: widget.cellStyle,
onCellEditing: widget.onCellEditing,
onCellEditing: widget.onCellFocus,
popoverController: _popover,
cellControllerBuilder: widget.cellControllerBuilder,
);

View File

@ -157,7 +157,6 @@ class _TextField extends StatelessWidget {
options: state.options,
selectedOptionMap: optionMap,
distanceToText: _editorPanelWidth * 0.7,
maxLength: 30,
tagController: tagController,
textSeparators: const [','],
onClick: () => popoverMutex.close(),

View File

@ -41,13 +41,16 @@ class GridTextCell extends GridCellWidget {
}
@override
GridFocusNodeCellState<GridTextCell> createState() => _GridTextCellState();
GridEditableTextCell<GridTextCell> createState() => _GridTextCellState();
}
class _GridTextCellState extends GridFocusNodeCellState<GridTextCell> {
class _GridTextCellState extends GridEditableTextCell<GridTextCell> {
late TextCellBloc _cellBloc;
late TextEditingController _controller;
@override
SingleListenerFocusNode focusNode = SingleListenerFocusNode();
@override
void initState() {
final cellController =

View File

@ -108,11 +108,14 @@ class GridURLCell extends GridCellWidget {
}
}
class _GridURLCellState extends GridFocusNodeCellState<GridURLCell> {
class _GridURLCellState extends GridEditableTextCell<GridURLCell> {
final _popoverController = PopoverController();
late final URLCellBloc _cellBloc;
late final TextEditingController _controller;
@override
SingleListenerFocusNode focusNode = SingleListenerFocusNode();
@override
void initState() {
super.initState();
@ -182,7 +185,7 @@ class _GridURLCellState extends GridFocusNodeCellState<GridURLCell> {
@override
void requestBeginFocus() {
widget.onCellEditing.value = true;
widget.onCellFocus.value = true;
_popoverController.show();
}

View File

@ -85,8 +85,8 @@ class _PropertyCellState extends State<_PropertyCell> {
final cell = widget.cellBuilder.build(widget.cellContext, style: style);
final gesture = GestureDetector(
behavior: HitTestBehavior.translucent,
onTap: () => cell.beginFocus.notify(),
behavior: HitTestBehavior.opaque,
onTap: () => cell.requestFocus.notify(),
child: AccessoryHover(
contentPadding: const EdgeInsets.symmetric(horizontal: 3, vertical: 3),
child: cell,
@ -97,8 +97,8 @@ class _PropertyCellState extends State<_PropertyCell> {
child: ConstrainedBox(
constraints: const BoxConstraints(minHeight: 30),
child: Row(
crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start,
children: [
AppFlowyPopover(
controller: popover,
@ -108,6 +108,7 @@ class _PropertyCellState extends State<_PropertyCell> {
popupBuilder: (popoverContext) => buildFieldEditor(),
child: SizedBox(
width: 150,
height: 40,
child: FieldCellButton(
field: widget.cellContext.fieldInfo.field,
onTap: () => popover.show(),

View File

@ -105,7 +105,7 @@ checksum = "9c7d0618f0e0b7e8ff11427422b64564d5fb0be1940354bfe2e0529b18a9d9b8"
[[package]]
name = "appflowy-integrate"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=e9f7fc#e9f7fc76192f2dbb2379f1a02310047763bd4426"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=06e942#06e942cb6433c94b5ecfe1d431b64bba625fc09c"
dependencies = [
"anyhow",
"collab",
@ -1030,7 +1030,7 @@ dependencies = [
[[package]]
name = "collab"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=e9f7fc#e9f7fc76192f2dbb2379f1a02310047763bd4426"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=06e942#06e942cb6433c94b5ecfe1d431b64bba625fc09c"
dependencies = [
"anyhow",
"bytes",
@ -1048,7 +1048,7 @@ dependencies = [
[[package]]
name = "collab-client-ws"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=e9f7fc#e9f7fc76192f2dbb2379f1a02310047763bd4426"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=06e942#06e942cb6433c94b5ecfe1d431b64bba625fc09c"
dependencies = [
"bytes",
"collab-sync",
@ -1066,7 +1066,7 @@ dependencies = [
[[package]]
name = "collab-database"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=e9f7fc#e9f7fc76192f2dbb2379f1a02310047763bd4426"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=06e942#06e942cb6433c94b5ecfe1d431b64bba625fc09c"
dependencies = [
"anyhow",
"async-trait",
@ -1092,7 +1092,7 @@ dependencies = [
[[package]]
name = "collab-derive"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=e9f7fc#e9f7fc76192f2dbb2379f1a02310047763bd4426"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=06e942#06e942cb6433c94b5ecfe1d431b64bba625fc09c"
dependencies = [
"proc-macro2",
"quote",
@ -1104,7 +1104,7 @@ dependencies = [
[[package]]
name = "collab-document"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=e9f7fc#e9f7fc76192f2dbb2379f1a02310047763bd4426"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=06e942#06e942cb6433c94b5ecfe1d431b64bba625fc09c"
dependencies = [
"anyhow",
"collab",
@ -1122,7 +1122,7 @@ dependencies = [
[[package]]
name = "collab-folder"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=e9f7fc#e9f7fc76192f2dbb2379f1a02310047763bd4426"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=06e942#06e942cb6433c94b5ecfe1d431b64bba625fc09c"
dependencies = [
"anyhow",
"chrono",
@ -1142,7 +1142,7 @@ dependencies = [
[[package]]
name = "collab-persistence"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=e9f7fc#e9f7fc76192f2dbb2379f1a02310047763bd4426"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=06e942#06e942cb6433c94b5ecfe1d431b64bba625fc09c"
dependencies = [
"bincode",
"chrono",
@ -1162,7 +1162,7 @@ dependencies = [
[[package]]
name = "collab-plugins"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=e9f7fc#e9f7fc76192f2dbb2379f1a02310047763bd4426"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=06e942#06e942cb6433c94b5ecfe1d431b64bba625fc09c"
dependencies = [
"anyhow",
"async-trait",
@ -1193,7 +1193,7 @@ dependencies = [
[[package]]
name = "collab-sync"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=e9f7fc#e9f7fc76192f2dbb2379f1a02310047763bd4426"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=06e942#06e942cb6433c94b5ecfe1d431b64bba625fc09c"
dependencies = [
"bytes",
"collab",