Skip to content
代码片段 群组 项目
提交 8c99b59a 编辑于 作者: Neil Wang's avatar Neil Wang 提交者: 万友 朱
浏览文件

feat: #249 用户对子组/项目议题列表进行搜索

上级 37475d00
No related branches found
No related tags found
无相关合并请求
......@@ -28,7 +28,7 @@ class _SearchBoxState extends State<SearchBox> {
onSubmitted: widget.onSubmitted,
controller: widget.searchController,
decoration: InputDecoration(
hintText: 'Search (Only support English word now)',
hintText: 'Search',
contentPadding: const EdgeInsets.symmetric(vertical: 0, horizontal: 16),
suffixIcon: IconButton(
onPressed: () {
......
......@@ -8,10 +8,6 @@ class Assignee {
Assignee._(this.id, this.name, this.username, this.avatarUrl);
factory Assignee.init(int id, String name, String username, String avatarUrl) {
return Assignee._(id, name, username, avatarUrl);
}
factory Assignee.empty() {
return Assignee.fromJson({});
}
......
import 'package:jihu_gitlab_app/core/id.dart';
import 'package:jihu_gitlab_app/core/net/http_client.dart';
import 'package:jihu_gitlab_app/core/time.dart';
import 'package:jihu_gitlab_app/domain/action_name.dart';
import 'package:jihu_gitlab_app/domain/project.dart';
import 'package:jihu_gitlab_app/domain/target.dart';
......@@ -14,31 +13,13 @@ class Issue {
String title;
String targetUrl;
String createdAt;
Time _updatedAt;
ActionName actionName;
String targetType;
Target target;
Project project;
Assignee author;
Issue._(this.id, this.projectId, this.iid, this.title, this.targetUrl, this.createdAt, this._updatedAt, this.actionName, this.target, this.project, this.author, this.targetType);
factory Issue.fromJson(Map<String, dynamic> json) {
return Issue._(
Id.build(json['id']),
json['project_id'] ?? 0,
json['iid'] ?? 0,
json['title'] ?? '',
json['target_url'] ?? '',
json['created_at'] ?? '',
Time.init(json['updated_at'] ?? ''),
ActionName.init(json['action_name'] ?? ""),
Target.fromJson(json['target'] ?? {}),
Project.fromJson(json['project'] ?? {}),
Assignee.fromJson(json['author'] ?? {}),
json['target_type'] ?? '',
);
}
Issue._(this.id, this.projectId, this.iid, this.title, this.targetUrl, this.createdAt, this.actionName, this.target, this.project, this.author, this.targetType);
factory Issue.fromGraphQLJson(Map<String, dynamic> json) {
return Issue._(
......@@ -48,7 +29,6 @@ class Issue {
json['title'] ?? '',
'',
json['createdAt'] ?? '',
Time.init(json['updatedAt'] ?? ''),
ActionName.init(""),
Target.fromJson({'iid': int.parse(json['iid'] ?? '0'), 'title': json['title'] ?? '', 'created_at': json['createdAt'] ?? ''}),
Project.fromJson({}),
......@@ -57,8 +37,6 @@ class Issue {
);
}
Time get updatedAt => _updatedAt;
Future<void> readProject(Function setState) async {
var response = await HttpClient.instance().get("/api/v4/projects/$projectId");
project = Project.fromJson(response.body());
......
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/settings/settings_provider.dart';
import 'package:jihu_gitlab_app/core/user_provider.dart';
import 'package:jihu_gitlab_app/core/widgets/http_fail_view.dart';
import 'package:jihu_gitlab_app/core/widgets/no_data_view.dart';
......@@ -215,7 +216,7 @@ class SubgroupIssuesPageState extends State<SubgroupIssuesPage> {
),
ClipOval(
child: Image.network(
_model.issues[index].author.avatarUrl,
SettingsProvider().baseUrl.get + _model.issues[index].author.avatarUrl,
width: 16,
errorBuilder: (buildContext, object, trace) => Row(),
),
......
......@@ -9,31 +9,22 @@ import 'package:pull_to_refresh/pull_to_refresh.dart';
class ProjectIssuesModel {
static const int _size = 10;
int _page = 1;
bool _hasNextPage = false;
LoadState _loadState = LoadState.noItemState;
List<Issue> issues = [];
late int _projectId;
String? _afterCursor;
bool _hasNextPage = true;
final RefreshController _refreshController = RefreshController(initialRefresh: UserProvider.authorized);
final TextEditingController _searchController = TextEditingController();
Future<List<Issue>> _getIssues(String text, [int? size]) async {
var path = "/api/v4/projects/$_projectId/issues?search=$text&page=$_page&per_page=${size ?? _size}";
return HttpClient.instance().get<List<dynamic>>(path).then((response) {
return response.body().map((e) {
var issue = Issue.fromJson(e);
issue.project.id = _projectId;
return issue;
}).toList();
});
}
Future<bool> refresh(String text, Function setState) async {
Future<bool> refresh(String text, String fullPath, Function setState) async {
try {
_page = 1;
issues = await _getIssues(text, 20);
_hasNextPage = true;
_afterCursor = null;
issues = await _getIssues(text, fullPath, 20);
for (Issue element in issues) {
element.readProject(setState);
}
_loadState = LoadState.successState;
_hasNextPage = issues.length >= 20;
return Future.value(true);
} catch (e) {
debugPrint("ProjectIssuesModel: $e");
......@@ -42,21 +33,153 @@ class ProjectIssuesModel {
}
}
Future<bool> loadMore(String text, Function setState) async {
Future<bool> loadMore(String text, String fullPath, Function setState) async {
var hasNextPage = _hasNextPage;
var afterCursor = _afterCursor;
try {
_page++;
List<Issue> moreIssues = await _getIssues(text);
List<Issue> moreIssues = await _getIssues(text, fullPath);
for (Issue element in moreIssues) {
element.readProject(setState);
}
issues.addAll(moreIssues);
_hasNextPage = moreIssues.length >= _size;
return Future.value(true);
} catch (e) {
_page--;
_hasNextPage = hasNextPage;
_afterCursor = afterCursor;
return Future.value(false);
}
}
set projectId(int value) {
_projectId = value;
Future<List<Issue>> _getIssues(String text, String fullPath, [int? size]) async {
return HttpClient.instance().post<dynamic>('/api/graphql', {
"query":
'''query getIssuesEE(\$isProject: Boolean = false, \$fullPath: ID!, \$iid: String, \$search: String, \$sort: IssueSort, \$state: IssuableState, \$in: [IssuableSearchableField!], \$assigneeId: String, \$assigneeUsernames: [String!], \$authorUsername: String, \$confidential: Boolean, \$labelName: [String], \$milestoneTitle: [String], \$milestoneWildcardId: MilestoneWildcardId, \$myReactionEmoji: String, \$releaseTag: [String!], \$releaseTagWildcardId: ReleaseTagWildcardId, \$types: [IssueType!], \$epicId: String, \$iterationId: [ID], \$iterationWildcardId: IterationWildcardId, \$weight: String, \$healthStatus: HealthStatusFilter, \$crmContactId: String, \$crmOrganizationId: String, \$not: NegatedIssueFilterInput, \$or: UnionedIssueFilterInput, \$beforeCursor: String, \$afterCursor: String, \$firstPageSize: Int, \$lastPageSize: Int) {
group(fullPath: \$fullPath) @skip(if: \$isProject) {
id
issues(
includeSubepics: true
includeSubgroups: true
iid: \$iid
search: \$search
sort: \$sort
state: \$state
in: \$in
assigneeId: \$assigneeId
assigneeUsernames: \$assigneeUsernames
authorUsername: \$authorUsername
confidential: \$confidential
labelName: \$labelName
milestoneTitle: \$milestoneTitle
milestoneWildcardId: \$milestoneWildcardId
myReactionEmoji: \$myReactionEmoji
types: \$types
epicId: \$epicId
iterationId: \$iterationId
iterationWildcardId: \$iterationWildcardId
weight: \$weight
healthStatusFilter: \$healthStatus
crmContactId: \$crmContactId
crmOrganizationId: \$crmOrganizationId
not: \$not
or: \$or
before: \$beforeCursor
after: \$afterCursor
first: \$firstPageSize
last: \$lastPageSize
) {
pageInfo {
...PageInfo
}
nodes {
...IssueFragment
}
}
}
project(fullPath: \$fullPath) @include(if: \$isProject) {
id
issues(
includeSubepics: true
iid: \$iid
search: \$search
sort: \$sort
state: \$state
in: \$in
assigneeId: \$assigneeId
assigneeUsernames: \$assigneeUsernames
authorUsername: \$authorUsername
confidential: \$confidential
labelName: \$labelName
milestoneTitle: \$milestoneTitle
milestoneWildcardId: \$milestoneWildcardId
myReactionEmoji: \$myReactionEmoji
releaseTag: \$releaseTag
releaseTagWildcardId: \$releaseTagWildcardId
types: \$types
epicId: \$epicId
iterationId: \$iterationId
iterationWildcardId: \$iterationWildcardId
weight: \$weight
healthStatusFilter: \$healthStatus
crmContactId: \$crmContactId
crmOrganizationId: \$crmOrganizationId
not: \$not
or: \$or
before: \$beforeCursor
after: \$afterCursor
first: \$firstPageSize
last: \$lastPageSize
) {
pageInfo {
...PageInfo
}
nodes {
...IssueFragment
}
}
}
}
fragment PageInfo on PageInfo {
hasNextPage
startCursor
endCursor
}
fragment IssueFragment on Issue {
id
iid
projectId
createdAt
title
updatedAt
type
author {
id
avatarUrl
name
username
webUrl
}
}
''',
"variables": {
"isProject": true,
"fullPath": fullPath,
"search": text,
"sort": "CREATED_DESC",
"state": "opened",
"firstPageSize": size ?? _size,
"types": ["ISSUE", "INCIDENT", "TEST_CASE", "TASK"],
"afterCursor": _afterCursor,
"beforeCursor": null,
},
"operationName": "getIssuesEE"
}).then((response) {
var content = response.body()['data']['project']['issues'];
_hasNextPage = content['pageInfo']['hasNextPage'];
_afterCursor = content['pageInfo']['endCursor'];
return content['nodes'].map<Issue>((e) => Issue.fromGraphQLJson(e)).toList();
});
}
LoadState get loadState {
......
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/settings/settings_provider.dart';
import 'package:jihu_gitlab_app/core/user_provider.dart';
import 'package:jihu_gitlab_app/core/widgets/common_app_bar.dart';
import 'package:jihu_gitlab_app/core/widgets/http_fail_view.dart';
......@@ -29,7 +30,6 @@ class _ProjectIssuesPageState extends State<ProjectIssuesPage> {
@override
void initState() {
_model.projectId = widget.arguments['projectId'];
super.initState();
}
......@@ -47,7 +47,7 @@ class _ProjectIssuesPageState extends State<ProjectIssuesPage> {
}
void _onRefresh() async {
bool success = await _model.refresh(_model.searchController.text, () => {setState(() {})});
bool success = await _model.refresh(_model.searchController.text, widget.arguments['relativePath'], () => {setState(() {})});
setState(() {});
if (success) {
_model.refreshController.refreshCompleted(resetFooterState: true);
......@@ -153,7 +153,7 @@ class _ProjectIssuesPageState extends State<ProjectIssuesPage> {
}
void _loadMore() async {
bool success = await _model.loadMore(_model.searchController.text, () => setState(() {}));
bool success = await _model.loadMore(_model.searchController.text, widget.arguments['relativePath'], () => setState(() {}));
setState(() {});
if (success) {
if (_model.hasNextPage) {
......@@ -232,12 +232,12 @@ class _ProjectIssuesPageState extends State<ProjectIssuesPage> {
Container(
margin: const EdgeInsets.fromLTRB(0, 4, 0, 4),
child: Row(
children: [
children: const [
Expanded(
child: Text.rich(
TextSpan(
text: _model.issues[index].target.title,
style: const TextStyle(
text: '',
style: TextStyle(
fontSize: 14,
color: Color(0xFF5C5963),
),
......@@ -262,7 +262,7 @@ class _ProjectIssuesPageState extends State<ProjectIssuesPage> {
),
ClipOval(
child: Image.network(
_model.issues[index].author.avatarUrl,
SettingsProvider().baseUrl.get + _model.issues[index].author.avatarUrl,
width: 16,
errorBuilder: (buildContext, object, trace) => Row(),
),
......
......@@ -223,7 +223,7 @@ void main() {
await tester.enterText(find.byType(TextField), 'a');
await tester.tap(find.byIcon(Icons.clear));
await tester.pumpAndSettle();
expect(find.text('Search (Only support English word now)'), findsOneWidget);
expect(find.text('Search'), findsOneWidget);
await tester.enterText(find.byType(TextField), 'a');
await tester.testTextInput.receiveAction(TextInputAction.done);
await tester.pumpAndSettle();
......@@ -311,7 +311,7 @@ void main() {
await tester.enterText(find.byType(TextField), 'a');
await tester.tap(find.byIcon(Icons.clear));
await tester.pumpAndSettle();
expect(find.text('Search (Only support English word now)'), findsOneWidget);
expect(find.text('Search'), findsOneWidget);
await tester.enterText(find.byType(TextField), 'a');
await tester.testTextInput.receiveAction(TextInputAction.done);
await tester.pumpAndSettle();
......
0% 加载中 .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册