diff --git a/lib/core/widgets/markdown_input_box.dart b/lib/core/widgets/markdown_input_box.dart index 759f0720f1a5526e07baafb1245d7936250ff71c..7ba9d7040de2d563c700e75aef111fc6afcdb8a1 100644 --- a/lib/core/widgets/markdown_input_box.dart +++ b/lib/core/widgets/markdown_input_box.dart @@ -27,6 +27,7 @@ class MarkdownInputBox extends StatefulWidget { class _MarkdownInputBoxState extends State<MarkdownInputBox> with TickerProviderStateMixin { int _selectedTabBarIndex = 0; UploadedFileInfo? _uploadedFileInfo; + int _lastSelectionOffset = 0; @override Widget build(BuildContext context) { @@ -106,7 +107,7 @@ class _MarkdownInputBoxState extends State<MarkdownInputBox> with TickerProvider onChanged: (text) async { if (text == '') return; var index = widget.controller.selection.base.offset; - if (text[index - 1] == '@') { + if (text[index - 1] == '@' && index >= _lastSelectionOffset) { List<Member> result = await _openMemberSelector(); if (result.isNotEmpty) { var selectedUsername = '${result[0].username} '; @@ -118,6 +119,7 @@ class _MarkdownInputBoxState extends State<MarkdownInputBox> with TickerProvider ); } } + _lastSelectionOffset = index; setState(() {}); }, style: textTheme.bodyMedium, diff --git a/lib/modules/issues/issue_creation_view.dart b/lib/modules/issues/issue_creation_view.dart index 094c8de4d48f3225cca23151bc8bb2d27687e777..ee6b2b9f98231f972233f1b67c225865dbc4eb60 100644 --- a/lib/modules/issues/issue_creation_view.dart +++ b/lib/modules/issues/issue_creation_view.dart @@ -29,7 +29,7 @@ class _IssueCreationViewState extends State<IssueCreationView> { child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ InputFieldWithTitle(title: 'Title (required)', placeHolder: 'Write a issue title', controller: widget.issueTitleController), _buildTitle("Description"), - _buildDescriptionSelector(), + _buildDescriptionTemplatesSelector(), MarkdownInputBox(projectId: widget.projectId, controller: widget.issueDescriptionController), // _buildTitle("Assignees"), // _buildAssigneeSelector(), @@ -166,7 +166,7 @@ class _IssueCreationViewState extends State<IssueCreationView> { return AvatarAndName(username: member.username, avatarUrl: member.avatarUrl); } - Widget _buildDescriptionSelector() { + Widget _buildDescriptionTemplatesSelector() { var iconButton = IconButton( key: const Key("goto-description-selector"), onPressed: () async { diff --git a/lib/modules/todo_list/widgets/discussions_page.dart b/lib/modules/todo_list/widgets/discussions_page.dart index 04d6aa57c3b80b8fa471146fc946fd2b1a40e9e7..5210a87ad44c918c38fe74a8bcf3724f9d380791 100644 --- a/lib/modules/todo_list/widgets/discussions_page.dart +++ b/lib/modules/todo_list/widgets/discussions_page.dart @@ -5,8 +5,8 @@ import 'package:jihu_gitlab_app/core/widgets/avatar/avatar.dart'; import 'package:jihu_gitlab_app/core/widgets/avatar_and_name.dart'; import 'package:jihu_gitlab_app/core/widgets/common_app_bar.dart'; import 'package:jihu_gitlab_app/core/widgets/loading_button.dart'; -import 'package:jihu_gitlab_app/core/widgets/toast.dart'; import 'package:jihu_gitlab_app/core/widgets/selector/selector.dart'; +import 'package:jihu_gitlab_app/core/widgets/toast.dart'; import 'package:jihu_gitlab_app/domain/discussions.dart'; import 'package:jihu_gitlab_app/domain/note.dart'; import 'package:jihu_gitlab_app/modules/todo_list/todo_param.dart'; @@ -30,6 +30,7 @@ class _DiscussionsPageState extends State<DiscussionsPage> { FocusNode commentFocusNode = FocusNode(); final DiscussionsModel _model = DiscussionsModel(); String placeholder = "Write a comment ..."; + int inputTextLength = 0; @override void initState() { @@ -135,15 +136,16 @@ class _DiscussionsPageState extends State<DiscussionsPage> { decoration: const BoxDecoration(color: Color(0xF8F8FAFF), border: Border(top: BorderSide(width: 0.2, color: Color(0xFFCECECE)))), child: Row(children: [ Flexible(child: _buildInputView()), - TextButton( - onPressed: () async { - List<Member> result = await _openMemberSelector(); - if (result.isNotEmpty) { - var selectedUsername = result[0].username; - _commentController.text = "${_commentController.text}@$selectedUsername "; - } - }, - child: const Text("@", style: TextStyle(color: Color(0xFF66696D), fontSize: 20, fontWeight: FontWeight.bold))), + IconButton( + icon: const Icon(Icons.alternate_email), + onPressed: () async { + List<Member> result = await _openMemberSelector(); + if (result.isNotEmpty) { + var selectedUsername = result[0].username; + _commentController.text = "${_commentController.text}@$selectedUsername "; + } + }, + ), _buildAddCommentButton() ]), ), @@ -161,7 +163,7 @@ class _DiscussionsPageState extends State<DiscussionsPage> { controller: _commentController, focusNode: commentFocusNode, onChanged: (text) async { - if (text.endsWith("@")) { + if (text.endsWith("@") && text.length > inputTextLength) { List<Member> result = await _openMemberSelector(); if (result.isNotEmpty) { var selectedUsername = result[0].username; @@ -171,6 +173,7 @@ class _DiscussionsPageState extends State<DiscussionsPage> { setState(() { _model.submittable = text.isNotEmpty; }); + inputTextLength = text.length; }, cursorColor: const Color(0xFFFC6D26), decoration: InputDecoration(hintText: placeholder, border: InputBorder.none, hintStyle: const TextStyle(fontSize: 12, fontWeight: FontWeight.w400)), diff --git a/test/modules/todo_list/discussions_page_test.dart b/test/modules/todo_list/discussions_page_test.dart index 9ec5f3eea03900d9c41fa17b6d6f495b8dada206..c98d45e31b3a84ce8de2f1ff5b4dc6f451680f31 100644 --- a/test/modules/todo_list/discussions_page_test.dart +++ b/test/modules/todo_list/discussions_page_test.dart @@ -78,10 +78,45 @@ void main() { )); await tester.pumpAndSettle(); - await tester.tap(find.widgetWithText(TextButton, '@')); + await tester.tap(find.byIcon(Icons.alternate_email)); await tester.pumpAndSettle(); expect(find.byKey(const Key('member-selector')), findsOneWidget); }); + + testWidgets('Should be able display the member selector when enter the @ symbol', (WidgetTester tester) async { + TokenProvider().reset(Tester.token()); + UserProvider().reset(Tester.user()); + + when(client.get<List<dynamic>>("/api/v4/projects/72936/issues/265567/discussions")).thenAnswer((_) => Future(() => Response.of<List<dynamic>>(discussionsData))); + when(client.get<Map<String, dynamic>>("/api/v4/projects/72936/issues/265567")).thenAnswer((_) => Future(() => Response.of<Map<String, dynamic>>(issueData))); + when(client.get<Map<String, dynamic>>("/api/v4/projects/72936")).thenAnswer((_) => Future(() => Response.of<Map<String, dynamic>>(projectData))); + when(client.get<List<dynamic>>("/api/v4/projects/72936/members/all?page=1&per_page=50")).thenAnswer((_) async => Future.value(Response.of(members))); + HttpClient.setInstance(client); + + await tester.pumpWidget(MaterialApp( + home: Scaffold( + body: DiscussionsPage(param: TodoParam(72936, 6, 265567, "ultimate-plan/jihu-gitlab-app/demo-mr-test")), + ), + )); + + await tester.pumpAndSettle(); + expect(find.byType(TextField), findsOneWidget); + await tester.enterText(find.byType(TextField), "@"); + + // await tester.tap(find.byIcon(Icons.alternate_email)); + await tester.pumpAndSettle(); + expect(find.byKey(const Key('member-selector')), findsOneWidget); + expect(find.byType(BackButton), findsOneWidget); + await tester.tap(find.byType(BackButton)); + await tester.pumpAndSettle(); + // await tester.enterText(find.byType(TextField), "a"); + var finder = find.byType(TextField); + var textField = finder.evaluate().single.widget as TextField; + expect(textField.controller?.text, "@"); + textField.controller?.text = "@a"; + textField.controller?.text = "@"; + expect(find.byKey(const Key('member-selector')), findsNothing); + }); } List<dynamic> discussionsData = [