diff --git a/frontend/appflowy_flutter/integration_test/mobile/document/simple_table_test.dart b/frontend/appflowy_flutter/integration_test/mobile/document/simple_table_test.dart index d6ffc5d57f..9fa8be3a9c 100644 --- a/frontend/appflowy_flutter/integration_test/mobile/document/simple_table_test.dart +++ b/frontend/appflowy_flutter/integration_test/mobile/document/simple_table_test.dart @@ -2,9 +2,10 @@ import 'dart:async'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_widgets/_simple_table_bottom_sheet_actions.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; +import 'package:flutter/cupertino.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; @@ -244,6 +245,53 @@ void main() { expect(table.isHeaderColumnEnabled, isTrue); expect(table.isHeaderRowEnabled, isTrue); + // disable header column + { + // focus on the first cell + unawaited( + editorState.updateSelectionWithReason( + Selection.collapsed(Position(path: firstParagraphPath)), + reason: SelectionUpdateReason.uiEvent, + ), + ); + await tester.pumpAndSettle(); + + // click the row menu button + await tester.clickColumnMenuButton(0); + + final toggleButton = find.descendant( + of: find.byType(SimpleTableHeaderActionButton), + matching: find.byType(CupertinoSwitch), + ); + await tester.tapButton(toggleButton); + } + + // enable header row + { + // focus on the first cell + unawaited( + editorState.updateSelectionWithReason( + Selection.collapsed(Position(path: firstParagraphPath)), + reason: SelectionUpdateReason.uiEvent, + ), + ); + await tester.pumpAndSettle(); + + // click the row menu button + await tester.clickRowMenuButton(0); + + // enable header column + final toggleButton = find.descendant( + of: find.byType(SimpleTableHeaderActionButton), + matching: find.byType(CupertinoSwitch), + ); + await tester.tapButton(toggleButton); + } + + // check the table is updated + expect(table.isHeaderColumnEnabled, isFalse); + expect(table.isHeaderRowEnabled, isFalse); + // set to page width { final table = editorState.getNodeAtPath([0])!; diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_constants.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_constants.dart index 559b4d202d..bc6e3450e5 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_constants.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_constants.dart @@ -86,6 +86,9 @@ class SimpleTableContext { /// This value is available on mobile only final ValueNotifier isReorderingHitIndex = ValueNotifier(null); + /// Scroll controller for the table + ScrollController? horizontalScrollController; + void _onHoveringOnColumnsAndRowsChanged() { if (!_enableTableDebugLog) { return; diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_operations/simple_table_style_operation.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_operations/simple_table_style_operation.dart index 48ffaae3cd..59352af0ac 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_operations/simple_table_style_operation.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_operations/simple_table_style_operation.dart @@ -117,6 +117,8 @@ extension TableOptionOperation on EditorState { required Node tableCellNode, required TableAlign align, }) async { + await clearColumnTextAlign(tableCellNode: tableCellNode); + final columnIndex = tableCellNode.columnIndex; await _updateTableAttributes( tableCellNode: tableCellNode, @@ -144,6 +146,8 @@ extension TableOptionOperation on EditorState { required Node tableCellNode, required TableAlign align, }) async { + await clearRowTextAlign(tableCellNode: tableCellNode); + final rowIndex = tableCellNode.rowIndex; await _updateTableAttributes( tableCellNode: tableCellNode, @@ -385,4 +389,67 @@ extension TableOptionOperation on EditorState { transaction.updateNode(parentTableNode, attributes); await apply(transaction); } + + /// Clear the text align of the column at the index where the table cell node is located. + Future clearColumnTextAlign({ + required Node tableCellNode, + }) async { + final parentTableNode = tableCellNode.parentTableNode; + if (parentTableNode == null) { + Log.warn('parent table node is null'); + return; + } + final columnIndex = tableCellNode.columnIndex; + final transaction = this.transaction; + for (var i = 0; i < parentTableNode.rowLength; i++) { + final cell = parentTableNode.getTableCellNode( + rowIndex: i, + columnIndex: columnIndex, + ); + if (cell == null) { + continue; + } + for (final child in cell.children) { + transaction.updateNode(child, { + blockComponentAlign: null, + }); + } + } + if (transaction.operations.isNotEmpty) { + await apply(transaction); + } + } + + /// Clear the text align of the row at the index where the table cell node is located. + Future clearRowTextAlign({ + required Node tableCellNode, + }) async { + final parentTableNode = tableCellNode.parentTableNode; + if (parentTableNode == null) { + Log.warn('parent table node is null'); + return; + } + final rowIndex = tableCellNode.rowIndex; + final transaction = this.transaction; + for (var i = 0; i < parentTableNode.columnLength; i++) { + final cell = parentTableNode.getTableCellNode( + rowIndex: rowIndex, + columnIndex: i, + ); + if (cell == null) { + continue; + } + for (final child in cell.children) { + transaction.updateNode( + child, + { + blockComponentAlign: null, + }, + ); + } + } + if (transaction.operations.isNotEmpty) { + await apply(transaction); + } + } } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_widgets/_desktop_simple_table_widget.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_widgets/_desktop_simple_table_widget.dart index 6e640e4561..3a3f02b530 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_widgets/_desktop_simple_table_widget.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_widgets/_desktop_simple_table_widget.dart @@ -47,8 +47,16 @@ class _DesktopSimpleTableWidgetState extends State { final scrollController = ScrollController(); late final editorState = context.read(); + @override + void initState() { + super.initState(); + + simpleTableContext.horizontalScrollController = scrollController; + } + @override void dispose() { + simpleTableContext.horizontalScrollController = null; scrollController.dispose(); super.dispose(); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_widgets/_mobile_simple_table_widget.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_widgets/_mobile_simple_table_widget.dart index 41ae29c61c..9b3ad2d652 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_widgets/_mobile_simple_table_widget.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_widgets/_mobile_simple_table_widget.dart @@ -47,8 +47,16 @@ class _MobileSimpleTableWidgetState extends State { final scrollController = ScrollController(); late final editorState = context.read(); + @override + void initState() { + super.initState(); + + simpleTableContext.horizontalScrollController = scrollController; + } + @override void dispose() { + simpleTableContext.horizontalScrollController = null; scrollController.dispose(); super.dispose(); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_widgets/_simple_table_bottom_sheet_actions.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_widgets/_simple_table_bottom_sheet_actions.dart index 2d8b9025cf..aae1acb68b 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_widgets/_simple_table_bottom_sheet_actions.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_widgets/_simple_table_bottom_sheet_actions.dart @@ -239,18 +239,20 @@ class SimpleTableInsertActions extends ISimpleTableBottomSheetActions { SimpleTableInsertAction( type: SimpleTableMoreAction.insertAbove, enableLeftBorder: true, - onTap: () => _onActionTap( + onTap: (increaseCounter) async => _onActionTap( context, - SimpleTableMoreAction.insertAbove, + type: SimpleTableMoreAction.insertAbove, + increaseCounter: increaseCounter, ), ), const HSpace(2), SimpleTableInsertAction( type: SimpleTableMoreAction.insertBelow, enableRightBorder: true, - onTap: () => _onActionTap( + onTap: (increaseCounter) async => _onActionTap( context, - SimpleTableMoreAction.insertBelow, + type: SimpleTableMoreAction.insertBelow, + increaseCounter: increaseCounter, ), ), ], @@ -260,18 +262,20 @@ class SimpleTableInsertActions extends ISimpleTableBottomSheetActions { SimpleTableInsertAction( type: SimpleTableMoreAction.insertLeft, enableLeftBorder: true, - onTap: () => _onActionTap( + onTap: (increaseCounter) async => _onActionTap( context, - SimpleTableMoreAction.insertLeft, + type: SimpleTableMoreAction.insertLeft, + increaseCounter: increaseCounter, ), ), const HSpace(2), SimpleTableInsertAction( type: SimpleTableMoreAction.insertRight, enableRightBorder: true, - onTap: () => _onActionTap( + onTap: (increaseCounter) async => _onActionTap( context, - SimpleTableMoreAction.insertRight, + type: SimpleTableMoreAction.insertRight, + increaseCounter: increaseCounter, ), ), ], @@ -279,7 +283,11 @@ class SimpleTableInsertActions extends ISimpleTableBottomSheetActions { }; } - void _onActionTap(BuildContext context, SimpleTableMoreAction type) { + Future _onActionTap( + BuildContext context, { + required SimpleTableMoreAction type, + required int increaseCounter, + }) async { final simpleTableContext = context.read(); final tableNode = cellNode.parentTableNode; if (tableNode == null) { @@ -291,34 +299,48 @@ class SimpleTableInsertActions extends ISimpleTableBottomSheetActions { case SimpleTableMoreAction.insertAbove: // update the highlight status for the selecting row simpleTableContext.selectingRow.value = cellNode.rowIndex + 1; - editorState.insertRowInTable( + await editorState.insertRowInTable( tableNode, cellNode.rowIndex, ); case SimpleTableMoreAction.insertBelow: - editorState.insertRowInTable( + await editorState.insertRowInTable( tableNode, cellNode.rowIndex + 1, ); + // scroll to the next cell position + editorState.scrollService?.scrollTo( + SimpleTableConstants.defaultRowHeight, + duration: Durations.short3, + ); case SimpleTableMoreAction.insertLeft: // update the highlight status for the selecting column simpleTableContext.selectingColumn.value = cellNode.columnIndex + 1; - editorState.insertColumnInTable( + await editorState.insertColumnInTable( tableNode, cellNode.columnIndex, ); case SimpleTableMoreAction.insertRight: - editorState.insertColumnInTable( + await editorState.insertColumnInTable( tableNode, cellNode.columnIndex + 1, ); + final horizontalScrollController = + simpleTableContext.horizontalScrollController; + if (horizontalScrollController != null) { + final previousWidth = horizontalScrollController.offset; + horizontalScrollController.jumpTo( + previousWidth + SimpleTableConstants.defaultColumnWidth, + ); + } + default: assert(false, 'Unsupported action: $type'); } } } -class SimpleTableInsertAction extends StatelessWidget { +class SimpleTableInsertAction extends StatefulWidget { const SimpleTableInsertAction({ super.key, required this.type, @@ -330,7 +352,16 @@ class SimpleTableInsertAction extends StatelessWidget { final SimpleTableMoreAction type; final bool enableLeftBorder; final bool enableRightBorder; - final void Function() onTap; + final ValueChanged onTap; + + @override + State createState() => + _SimpleTableInsertActionState(); +} + +class _SimpleTableInsertActionState extends State { + // used to count how many times the action is tapped + int increaseCounter = 0; @override Widget build(BuildContext context) { @@ -341,19 +372,19 @@ class SimpleTableInsertAction extends StatelessWidget { shape: _buildBorder(), ), child: AnimatedGestureDetector( - onTapUp: onTap, + onTapUp: () => widget.onTap(increaseCounter++), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Padding( padding: const EdgeInsets.all(1), child: FlowySvg( - type.leftIconSvg, + widget.type.leftIconSvg, size: const Size.square(22), ), ), FlowyText( - type.name, + widget.type.name, fontSize: 12, figmaLineHeight: 16, ), @@ -370,10 +401,10 @@ class SimpleTableInsertAction extends StatelessWidget { ); return RoundedRectangleBorder( borderRadius: BorderRadius.only( - topLeft: enableLeftBorder ? radius : Radius.zero, - bottomLeft: enableLeftBorder ? radius : Radius.zero, - topRight: enableRightBorder ? radius : Radius.zero, - bottomRight: enableRightBorder ? radius : Radius.zero, + topLeft: widget.enableLeftBorder ? radius : Radius.zero, + bottomLeft: widget.enableLeftBorder ? radius : Radius.zero, + topRight: widget.enableRightBorder ? radius : Radius.zero, + bottomRight: widget.enableRightBorder ? radius : Radius.zero, ), ); } @@ -592,7 +623,7 @@ class _SimpleTableHeaderActionButtonState child: CupertinoSwitch( value: value, activeColor: Theme.of(context).colorScheme.primary, - onChanged: (_) {}, + onChanged: (_) => _toggle(), ), ), ); @@ -1198,19 +1229,12 @@ class SimpleTableQuickActions extends StatelessWidget { SimpleTableMoreAction.copy, ), ), - FutureBuilder( - future: getIt().getData(), - builder: (context, snapshot) { - final hasContent = snapshot.data?.tableJson != null; - return SimpleTableQuickAction( - type: SimpleTableMoreAction.paste, - isEnabled: hasContent, - onTap: () => _onActionTap( - context, - SimpleTableMoreAction.paste, - ), - ); - }, + SimpleTableQuickAction( + type: SimpleTableMoreAction.paste, + onTap: () => _onActionTap( + context, + SimpleTableMoreAction.paste, + ), ), SimpleTableQuickAction( type: SimpleTableMoreAction.delete, diff --git a/frontend/appflowy_flutter/pubspec.lock b/frontend/appflowy_flutter/pubspec.lock index 1327e1dd41..70cc8edcfb 100644 --- a/frontend/appflowy_flutter/pubspec.lock +++ b/frontend/appflowy_flutter/pubspec.lock @@ -61,8 +61,8 @@ packages: dependency: "direct main" description: path: "." - ref: cfb8b1b - resolved-ref: cfb8b1b6eb06f73a4fb297b6fd1d54b0ccec2922 + ref: "9f6a299" + resolved-ref: "9f6a29968ecbb61678b8e0e8c9d90bcba44a24e3" url: "https://github.com/AppFlowy-IO/appflowy-editor.git" source: git version: "4.0.0" diff --git a/frontend/appflowy_flutter/pubspec.yaml b/frontend/appflowy_flutter/pubspec.yaml index f40d61b4b0..4c3e4c203f 100644 --- a/frontend/appflowy_flutter/pubspec.yaml +++ b/frontend/appflowy_flutter/pubspec.yaml @@ -174,7 +174,7 @@ dependency_overrides: appflowy_editor: git: url: https://github.com/AppFlowy-IO/appflowy-editor.git - ref: "cfb8b1b" + ref: "9f6a299" appflowy_editor_plugins: git: diff --git a/frontend/appflowy_flutter/test/unit_test/simple_table/simple_table_style_operation_test.dart b/frontend/appflowy_flutter/test/unit_test/simple_table/simple_table_style_operation_test.dart index 940f03711a..dd127d3d0b 100644 --- a/frontend/appflowy_flutter/test/unit_test/simple_table/simple_table_style_operation_test.dart +++ b/frontend/appflowy_flutter/test/unit_test/simple_table/simple_table_style_operation_test.dart @@ -1,5 +1,6 @@ import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table.dart'; import 'package:appflowy_backend/log.dart'; +import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:flutter_test/flutter_test.dart'; import 'simple_table_test_helper.dart'; @@ -193,5 +194,45 @@ void main() { expect(tableNode.tableAlign, align); } }); + + test('clear the existing align of the column before updating', () async { + final (editorState, tableNode) = createEditorStateAndTable( + rowCount: 2, + columnCount: 3, + ); + + final firstCellNode = tableNode.getTableCellNode( + rowIndex: 0, + columnIndex: 0, + ); + + Node firstParagraphNode = firstCellNode!.children.first; + + // format the first paragraph to center align + final transaction = editorState.transaction; + transaction.updateNode( + firstParagraphNode, + { + blockComponentAlign: TableAlign.right.key, + }, + ); + await editorState.apply(transaction); + + firstParagraphNode = editorState.getNodeAtPath([0, 0, 0, 0])!; + expect( + firstParagraphNode.attributes[blockComponentAlign], + TableAlign.right.key, + ); + + await editorState.updateColumnAlign( + tableCellNode: firstCellNode, + align: TableAlign.center, + ); + + expect( + firstParagraphNode.attributes[blockComponentAlign], + null, + ); + }); }); }