Skip to content
代码片段 群组 项目
提交 2896c9fe 编辑于 作者: Neil Wang's avatar Neil Wang 提交者: Raymond Liao
浏览文件

feat: #204 User @ people in create issue page

上级 43c3a1f3
No related branches found
No related tags found
无相关合并请求
......@@ -4,8 +4,12 @@ import 'package:jihu_gitlab_app/core/browser_launcher.dart';
import 'package:jihu_gitlab_app/core/file_uploader.dart';
import 'package:jihu_gitlab_app/core/loader.dart';
import 'package:jihu_gitlab_app/core/string_utils.dart';
import 'package:jihu_gitlab_app/core/widgets/avatar_and_name.dart';
import 'package:jihu_gitlab_app/core/widgets/photo_picker.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/modules/todo_list/widgets/member.dart';
import 'package:jihu_gitlab_app/modules/todo_list/widgets/member_provider.dart';
class MarkdownInputBox extends StatefulWidget {
const MarkdownInputBox({super.key, required this.controller, required this.projectId});
......@@ -44,6 +48,7 @@ class _MarkdownInputBoxState extends State<MarkdownInputBox> with TickerProvider
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Container(
alignment: Alignment.centerLeft,
constraints: const BoxConstraints(maxWidth: 150),
height: 40,
child: TabBar(
......@@ -64,6 +69,20 @@ class _MarkdownInputBoxState extends State<MarkdownInputBox> with TickerProvider
});
},
)),
const Flexible(fit: FlexFit.tight, child: SizedBox()),
InkWell(
onTap: () => _onAtButtonPressed(),
child: Container(
decoration: const BoxDecoration(color: Colors.white),
alignment: Alignment.centerRight,
height: 40,
width: 50,
child: const Icon(
Icons.alternate_email,
color: Colors.black26,
),
),
),
InkWell(
onTap: () => _onImageButtonPressed(),
child: Container(
......@@ -76,7 +95,7 @@ class _MarkdownInputBoxState extends State<MarkdownInputBox> with TickerProvider
color: Colors.black26,
),
),
)
),
],
),
const Divider(height: 0, thickness: 0.5),
......@@ -84,7 +103,21 @@ class _MarkdownInputBoxState extends State<MarkdownInputBox> with TickerProvider
if (_selectedTabBarIndex == 0) {
return TextField(
controller: widget.controller,
onChanged: (text) {
onChanged: (text) async {
if (text == '') return;
var index = widget.controller.selection.base.offset;
if (text[index - 1] == '@') {
List<Member> result = await _openMemberSelector();
if (result.isNotEmpty) {
var selectedUsername = '${result[0].username} ';
var finalText = StringUtils.add(text, selectedUsername, index);
widget.controller.text = finalText;
widget.controller.selection = TextSelection(
baseOffset: index + selectedUsername.length,
extentOffset: index + selectedUsername.length,
);
}
}
setState(() {});
},
style: textTheme.bodyMedium,
......@@ -154,6 +187,22 @@ class _MarkdownInputBoxState extends State<MarkdownInputBox> with TickerProvider
}
}
Future<void> _onAtButtonPressed() async {
var text = widget.controller.text;
var index = widget.controller.selection.base.offset;
List<Member> result = await _openMemberSelector();
if (result.isNotEmpty) {
var selectedUsername = '@${result[0].username} ';
var finalText = StringUtils.add(text, selectedUsername, index);
widget.controller.text = finalText;
widget.controller.selection = TextSelection(
baseOffset: index + selectedUsername.length,
extentOffset: index + selectedUsername.length,
);
}
setState(() {});
}
Future<void> _startUploadFiles() async {
if (PhotoPicker.imageFileList != null) {
debugPrint('_startUploadFiles not null');
......@@ -183,4 +232,18 @@ class _MarkdownInputBoxState extends State<MarkdownInputBox> with TickerProvider
});
}
}
Future<List<Member>> _openMemberSelector() async {
return (await Navigator.of(context).push(MaterialPageRoute(builder: (context) {
return Selector<int, Member>(
itemBuilder: (BuildContext context, int index, dynamic member) => AvatarAndName(username: member.username, avatarUrl: member.avatarUrl),
filter: (keyword, member) => member.username.toLowerCase().contains(keyword.toLowerCase()),
dataProvider: MemberProvider(projectId: widget.projectId),
keyMapper: (member) => member.id,
title: 'Select contact',
key: const Key('member-selector'),
);
})) ??
<Member>[]) as List<Member>;
}
}
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:jihu_gitlab_app/core/load_state.dart';
import 'package:jihu_gitlab_app/core/paged_state.dart';
import 'package:jihu_gitlab_app/core/widgets/common_app_bar.dart';
import 'package:jihu_gitlab_app/modules/issues/issue_creation_page.dart';
import 'package:jihu_gitlab_app/modules/projects/projects_selector/projects_selector_model.dart';
import 'package:provider/provider.dart';
import 'package:pull_to_refresh/pull_to_refresh.dart';
import '../../../core/token_provider.dart';
import '../../../core/widgets/unauthorized_view.dart';
class ProjectsSelector extends StatefulWidget {
final String fullPath;
......@@ -20,62 +14,16 @@ class ProjectsSelector extends StatefulWidget {
State<StatefulWidget> createState() => _ProjectsSelectorState();
}
class _ProjectsSelectorState extends State<ProjectsSelector> {
final RefreshController _refreshController = RefreshController(initialRefresh: TokenProvider.authorized);
class _ProjectsSelectorState extends PagedState<ProjectsSelector> {
final ProjectsSelectorModel _model = ProjectsSelectorModel();
@override
Widget build(BuildContext context) {
return Scaffold(appBar: CommonAppBar(title: const Text('Select project')), body: _buildBody());
}
Widget _buildBody() {
return Consumer(builder: (context, tokenProvider, child) {
if (!TokenProvider.authorized) {
return const UnauthorizedView();
}
if (_model.loadState == LoadState.errorState) {
return _buildHttpFailureView();
}
if (_model.loadState == LoadState.successState && _model.projects.isEmpty) {
return _buildEmptyDataView();
}
return _buildRefreshView();
});
}
Widget _buildRefreshView() {
return SmartRefresher(
controller: _refreshController,
enablePullDown: true,
enablePullUp: true,
header: const WaterDropHeader(),
footer: CustomFooter(
builder: (BuildContext context, LoadStatus? mode) {
Widget body;
if (mode == LoadStatus.idle) {
body = const Text("上拉加载更多...");
} else if (mode == LoadStatus.loading) {
body = const CupertinoActivityIndicator();
} else if (mode == LoadStatus.failed) {
body = const Text("加载失败,请重试");
} else if (mode == LoadStatus.canLoading) {
body = const Text("松开加载");
} else {
body = const Text("没有数据了");
}
return SizedBox(
height: 55.0,
child: Center(child: body),
);
},
),
onRefresh: _onRefresh,
child: _buildListView(),
);
AppBar? buildAppBar() {
return CommonAppBar(title: const Text('Select project'));
}
Widget _buildListView() {
@override
Widget? buildListView() {
return SafeArea(
child: Padding(
padding: const EdgeInsets.all(16),
......@@ -109,40 +57,18 @@ class _ProjectsSelectorState extends State<ProjectsSelector> {
)));
}
void _onRefresh() async {
bool success = await _model.refresh(widget.fullPath);
setState(() {});
if (success) {
_refreshController.refreshCompleted(resetFooterState: true);
} else {
_refreshController.refreshFailed();
}
@override
Function hasNextPage() {
return () => false;
}
Widget _buildEmptyDataView() {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Image.asset("assets/images/no_item.png", height: 40, width: 40),
Container(height: 5),
const Text("No to-do items", style: TextStyle(fontSize: 14, color: Color.fromRGBO(23, 19, 33, 1)))
],
),
);
@override
Future<bool> Function() modelLoadMore() {
return () => Future.value(true);
}
Widget _buildHttpFailureView() {
return Center(
heightFactor: 100,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Image.asset("assets/images/refreshException.png", height: 40, width: 40),
Container(height: 5),
const Text(" Refresh exception, please try again later ", style: TextStyle(fontSize: 14, color: Color.fromRGBO(23, 19, 33, 1)))
],
),
);
@override
Future<bool> Function() modelRefresh() {
return () => _model.refresh(widget.fullPath);
}
}
......@@ -6,5 +6,9 @@ void main() {
test('Expected light theme data', () {
const ColorScheme colorScheme = JiHuGitLabThemeData.lightColorScheme;
expect(colorScheme.background, const Color(0xFFF8F8FA));
var lightTheme = JiHuGitLabThemeData.lightThemeData;
expect(lightTheme.appBarTheme.backgroundColor, const Color(0xFFF8F8FA));
var darkTheme = JiHuGitLabThemeData.darkThemeData;
expect(darkTheme.appBarTheme.backgroundColor, const Color(0xFFF8F8FA));
});
}
......@@ -156,4 +156,36 @@ void main() {
TokenProvider().fullReset();
});
testWidgets('Should open member selector', (WidgetTester tester) async {
await TokenProvider().reset(Tester.token());
var mockHttpClient = MockHttpClient();
when(mockHttpClient.get<Map<String, dynamic>>("/api/v4/user")).thenAnswer((_) => Future(() => Response.of<Map<String, dynamic>>({'id': 5882})));
when(mockHttpClient.get<dynamic>("/api/v4/groups/0/iterations")).thenAnswer((_) async => Future(() => Response.of("")));
when(mockHttpClient.get<List<dynamic>>("/api/v4/projects/10000/members/all?page=1&per_page=50")).thenAnswer((_) async => Future.value(Response.of(members)));
HttpClient.setInstance(mockHttpClient);
MockNavigatorObserver navigatorObserver = MockNavigatorObserver();
await tester.pumpWidget(MultiProvider(
providers: [ChangeNotifierProvider(create: (context) => TokenProvider())],
child: MaterialApp(
navigatorObservers: [navigatorObserver],
onGenerateRoute: onGenerateRoute,
home: const Scaffold(
body: IssueCreationPage(projectId: 10000, from: 'group', groupId: 0),
),
),
));
expect(find.byIcon(Icons.alternate_email), findsOneWidget);
await tester.tap(find.byIcon(Icons.alternate_email));
for (int i = 0; i < 5; i++) {
await tester.pumpAndSettle();
}
expect(find.text("Select contact"), findsOneWidget);
verify(navigatorObserver.didPush(any, any));
expect(find.byType(TextField), findsOneWidget);
TokenProvider().fullReset();
});
}
......@@ -9,6 +9,7 @@ import 'package:jihu_gitlab_app/modules/auth/auth.dart';
import 'package:jihu_gitlab_app/modules/auth/login_page.dart';
import 'package:jihu_gitlab_app/modules/projects/projects_selector/projects_selector.dart';
import 'package:mockito/mockito.dart';
import 'package:provider/provider.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../../../core/net/http_request_test.mocks.dart';
......@@ -17,32 +18,34 @@ import '../../../mocker/tester.dart';
void main() {
testWidgets('Should preview unauthorized projects picker when do not login.', (tester) async {
await TokenProvider().restore();
await tester.pumpWidget(const MaterialApp(
home: Scaffold(
body: ProjectsSelector(
await tester.pumpWidget(MultiProvider(
providers: [ChangeNotifierProvider(create: (context) => TokenProvider())],
child: const MaterialApp(
home: Scaffold(
body: ProjectsSelector(
fullPath: 'ultimate-plan',
groupId: 0,
),
),
));
)))));
await tester.pumpAndSettle();
expect(find.byType(UnauthorizedView), findsOneWidget);
TokenProvider().fullReset();
});
testWidgets('Should display container when logged in', (tester) async {
late GlobalKey currentRouteKey;
await tester.pumpWidget(MaterialApp(
routes: {
SignInPage.routeName: (context) => SignInPage(key: currentRouteKey = GlobalKey(), arguments: const {'host': 'test.host', 'clientId': 'client_id'})
},
home: const Scaffold(
body: ProjectsSelector(
fullPath: 'ultimate-plan',
groupId: 0,
),
),
));
await tester.pumpWidget(MultiProvider(
providers: [ChangeNotifierProvider(create: (context) => TokenProvider())],
child: MaterialApp(
routes: {
SignInPage.routeName: (context) => SignInPage(key: currentRouteKey = GlobalKey(), arguments: const {'host': 'test.host', 'clientId': 'client_id'})
},
home: const Scaffold(
body: ProjectsSelector(
fullPath: 'ultimate-plan',
groupId: 0,
)))));
await tester.pumpAndSettle();
expect(find.byType(UnauthorizedView), findsOneWidget);
await tester.tap(find.byType(PopupMenuButton<String>));
......@@ -57,20 +60,24 @@ void main() {
Navigator.pop(currentRouteKey.currentContext!, true);
await tester.pumpAndSettle();
expect(find.text('Select project'), findsOneWidget);
TokenProvider().fullReset();
});
testWidgets('Should preview projects picker.', (tester) async {
await TokenProvider().reset(Tester.token());
await tester.pumpWidget(const MaterialApp(
home: Scaffold(
body: ProjectsSelector(
await tester.pumpWidget(MultiProvider(
providers: [ChangeNotifierProvider(create: (context) => TokenProvider())],
child: const MaterialApp(
home: Scaffold(
body: ProjectsSelector(
fullPath: 'ultimate-plan',
groupId: 0,
),
),
));
)))));
await tester.pumpAndSettle(const Duration(seconds: 1));
expect(find.text('Select project'), findsOneWidget);
TokenProvider().fullReset();
});
setUp(() async {
......
0% 加载中 .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册