From d74b1412791cf94af2ad570eb645ee19f7b9db17 Mon Sep 17 00:00:00 2001 From: Shreyas Thirumalai Date: Sun, 10 Jan 2021 11:50:03 +0530 Subject: [PATCH] feat(gitea): issue, issueComment, labels, refactor(GeIssueComment) (#155) * feat(gitea): issue, issueAdd, issueComment * rollback grade.properties change * refactor: GeIssueComment, fix: url GtIssueComment * feat: labels --- lib/models/auth.dart | 54 ++++++++++- lib/models/gitea.dart | 25 +++++ lib/models/gitea.g.dart | 49 +++++++++- lib/router.dart | 20 ++++ lib/screens/ge_issue_comment.dart | 58 +++-------- lib/screens/gt_issue.dart | 153 ++++++++++++++++++++++++++++++ lib/screens/gt_issue_comment.dart | 81 ++++++++++++++++ lib/screens/gt_issue_form.dart | 78 +++++++++++++++ lib/screens/gt_issues.dart | 22 ++++- 9 files changed, 489 insertions(+), 51 deletions(-) create mode 100644 lib/screens/gt_issue.dart create mode 100644 lib/screens/gt_issue_comment.dart create mode 100644 lib/screens/gt_issue_form.dart diff --git a/lib/models/auth.dart b/lib/models/auth.dart index 311ed3e..a275fe6 100644 --- a/lib/models/auth.dart +++ b/lib/models/auth.dart @@ -245,11 +245,55 @@ class AuthModel with ChangeNotifier { } } - Future fetchGitea(String p) async { - final res = await http.get('${activeAccount.domain}/api/v1$p', - headers: {'Authorization': 'token $token'}); - final info = json.decode(utf8.decode(res.bodyBytes)); - return info; + Future fetchGitea( + String p, { + requestType = 'GET', + Map body = const {}, + }) async { + http.Response res; + Map headers = { + 'Authorization': 'token $token', + HttpHeaders.contentTypeHeader: 'application/json' + }; + switch (requestType) { + case 'DELETE': + { + await http.delete( + '${activeAccount.domain}/api/v1$p', + headers: headers, + ); + break; + } + case 'POST': + { + res = await http.post( + '${activeAccount.domain}/api/v1$p', + headers: headers, + body: jsonEncode(body), + ); + break; + } + case 'PATCH': + { + res = await http.patch( + '${activeAccount.domain}/api/v1$p', + headers: headers, + body: jsonEncode(body), + ); + break; + } + default: + { + res = await http.get('${activeAccount.domain}/api/v1$p', + headers: headers); + break; + } + } + if (requestType != 'DELETE') { + final info = json.decode(utf8.decode(res.bodyBytes)); + return info; + } + return; } Future fetchGiteaWithPage(String path, diff --git a/lib/models/gitea.dart b/lib/models/gitea.dart index 56399a0..78a56a8 100644 --- a/lib/models/gitea.dart +++ b/lib/models/gitea.dart @@ -108,12 +108,23 @@ class GiteaIssue { GiteaUser user; int comments; DateTime updatedAt; + String state; String htmlUrl; + List labels; GiteaIssue(); factory GiteaIssue.fromJson(Map json) => _$GiteaIssueFromJson(json); } +@JsonSerializable(fieldRename: FieldRename.snake) +class GiteaLabel { + String color; + String name; + GiteaLabel(); + factory GiteaLabel.fromJson(Map json) => + _$GiteaLabelFromJson(json); +} + @JsonSerializable(fieldRename: FieldRename.snake) class GiteaHeatmapItem { int timestamp; @@ -122,3 +133,17 @@ class GiteaHeatmapItem { factory GiteaHeatmapItem.fromJson(Map json) => _$GiteaHeatmapItemFromJson(json); } + +@JsonSerializable(fieldRename: FieldRename.snake) +class GiteaComment { + String body; + DateTime createdAt; + String htmlUrl; + String originalAuthor; + DateTime updatedAt; + int id; + GiteaUser user; + GiteaComment(); + factory GiteaComment.fromJson(Map json) => + _$GiteaCommentFromJson(json); +} diff --git a/lib/models/gitea.g.dart b/lib/models/gitea.g.dart index a92f16b..5bb42d7 100644 --- a/lib/models/gitea.g.dart +++ b/lib/models/gitea.g.dart @@ -187,7 +187,12 @@ GiteaIssue _$GiteaIssueFromJson(Map json) { ..updatedAt = json['updated_at'] == null ? null : DateTime.parse(json['updated_at'] as String) - ..htmlUrl = json['html_url'] as String; + ..state = json['state'] as String + ..htmlUrl = json['html_url'] as String + ..labels = (json['labels'] as List) + ?.map((e) => + e == null ? null : GiteaLabel.fromJson(e as Map)) + ?.toList(); } Map _$GiteaIssueToJson(GiteaIssue instance) => @@ -198,7 +203,21 @@ Map _$GiteaIssueToJson(GiteaIssue instance) => 'user': instance.user, 'comments': instance.comments, 'updated_at': instance.updatedAt?.toIso8601String(), + 'state': instance.state, 'html_url': instance.htmlUrl, + 'labels': instance.labels, + }; + +GiteaLabel _$GiteaLabelFromJson(Map json) { + return GiteaLabel() + ..color = json['color'] as String + ..name = json['name'] as String; +} + +Map _$GiteaLabelToJson(GiteaLabel instance) => + { + 'color': instance.color, + 'name': instance.name, }; GiteaHeatmapItem _$GiteaHeatmapItemFromJson(Map json) { @@ -212,3 +231,31 @@ Map _$GiteaHeatmapItemToJson(GiteaHeatmapItem instance) => 'timestamp': instance.timestamp, 'contributions': instance.contributions, }; + +GiteaComment _$GiteaCommentFromJson(Map json) { + return GiteaComment() + ..body = json['body'] as String + ..createdAt = json['created_at'] == null + ? null + : DateTime.parse(json['created_at'] as String) + ..htmlUrl = json['html_url'] as String + ..originalAuthor = json['original_author'] as String + ..updatedAt = json['updated_at'] == null + ? null + : DateTime.parse(json['updated_at'] as String) + ..id = json['id'] as int + ..user = json['user'] == null + ? null + : GiteaUser.fromJson(json['user'] as Map); +} + +Map _$GiteaCommentToJson(GiteaComment instance) => + { + 'body': instance.body, + 'created_at': instance.createdAt?.toIso8601String(), + 'html_url': instance.htmlUrl, + 'original_author': instance.originalAuthor, + 'updated_at': instance.updatedAt?.toIso8601String(), + 'id': instance.id, + 'user': instance.user, + }; diff --git a/lib/router.dart b/lib/router.dart index ef136f9..09f0d20 100644 --- a/lib/router.dart +++ b/lib/router.dart @@ -35,6 +35,9 @@ import 'package:git_touch/screens/gh_org_repos.dart'; import 'package:git_touch/screens/gl_commit.dart'; import 'package:git_touch/screens/gl_starrers.dart'; import 'package:git_touch/screens/gt_commits.dart'; +import 'package:git_touch/screens/gt_issue.dart'; +import 'package:git_touch/screens/gt_issue_comment.dart'; +import 'package:git_touch/screens/gt_issue_form.dart'; import 'package:git_touch/screens/gt_issues.dart'; import 'package:git_touch/screens/gt_object.dart'; import 'package:git_touch/screens/gt_orgs.dart'; @@ -321,6 +324,9 @@ class GiteaRouter { GiteaRouter.commits, GiteaRouter.issues, GiteaRouter.pulls, + GiteaRouter.issueAdd, + GiteaRouter.issue, + GiteaRouter.issueComment, ]; static final status = RouterScreen('/status', (context, parameters) => GtStatusScreen()); @@ -387,6 +393,20 @@ class GiteaRouter { (context, parameters) => GtIssuesScreen( parameters['owner'].first, parameters['name'].first, isPr: true)); + static final issueAdd = RouterScreen( + '/:owner/:name/issues/new', + (context, parameters) => GtIssueFormScreen( + parameters['owner'].first, parameters['name'].first)); + static final issue = RouterScreen( + '/:owner/:name/issues/:number', + (context, parameters) => GtIssueScreen(parameters['owner'].first, + parameters['name'].first, parameters['number'].first)); + static final issueComment = RouterScreen( + '/:owner/:name/issues/:number/comment', + (context, parameters) => GtIssueCommentScreen(parameters['owner'].first, + parameters['name'].first, parameters['number'].first, + body: parameters['body'] != null ? parameters['body'].first : '', + id: parameters['id'] != null ? parameters['id'].first : '')); } class BitbucketRouter { diff --git a/lib/screens/ge_issue_comment.dart b/lib/screens/ge_issue_comment.dart index feb090c..9969272 100644 --- a/lib/screens/ge_issue_comment.dart +++ b/lib/screens/ge_issue_comment.dart @@ -53,53 +53,25 @@ class _GeIssueCommentScreenState extends State { CupertinoButton.filled( child: Text('Comment'), onPressed: () async { - if (!widget.isPr) { - if (!isEdit) { - final res = await auth.fetchGitee( - '/repos/${widget.owner}/${widget.name}/issues/${widget.number}/comments', - requestType: 'POST', - body: {'body': _controller.text, 'repo': widget.name}, - ); - } else { - final res = await auth.fetchGitee( - '/repos/${widget.owner}/${widget.name}/issues/comments/${int.parse(widget.id)}', - requestType: 'PATCH', - body: {'body': _controller.text, 'repo': widget.name}, - ); - } - Navigator.pop(context, ''); - await theme.push( - context, - '/gitee/${widget.owner}/${widget.name}/issues/${widget.number}', - replace: true, + if (!isEdit) { + final res = await auth.fetchGitee( + '/repos/${widget.owner}/${widget.name}/${widget.isPr ? 'pulls' : 'issues'}/${widget.number}/comments', + requestType: 'POST', + body: {'body': _controller.text, 'repo': widget.name}, ); } else { - if (!isEdit) { - final res = await auth.fetchGitee( - '/repos/${widget.owner}/${widget.name}/pulls/${widget.number}/comments', - requestType: 'POST', - body: {'body': _controller.text, 'repo': widget.name}, - ); - Navigator.pop(context, ''); - await theme.push( - context, - '/gitee/${widget.owner}/${widget.name}/pulls/${widget.number}', - replace: true, - ); - } else { - final res = await auth.fetchGitee( - '/repos/${widget.owner}/${widget.name}/pulls/comments/${int.parse(widget.id)}', - requestType: 'PATCH', - body: {'body': _controller.text, 'repo': widget.name}, - ); - } - Navigator.pop(context, ''); - await theme.push( - context, - '/gitee/${widget.owner}/${widget.name}/pulls/${widget.number}', - replace: true, + final res = await auth.fetchGitee( + '/repos/${widget.owner}/${widget.name}/${widget.isPr ? 'pulls' : 'issues'}/comments/${int.parse(widget.id)}', + requestType: 'PATCH', + body: {'body': _controller.text, 'repo': widget.name}, ); } + Navigator.pop(context, ''); + await theme.push( + context, + '/gitee/${widget.owner}/${widget.name}/${widget.isPr ? 'pulls' : 'issues'}/${widget.number}', + replace: true, + ); }, ), ], diff --git a/lib/screens/gt_issue.dart b/lib/screens/gt_issue.dart new file mode 100644 index 0000000..e71cdd8 --- /dev/null +++ b/lib/screens/gt_issue.dart @@ -0,0 +1,153 @@ +import 'package:flutter/material.dart'; +import 'package:git_touch/models/gitea.dart'; +import 'package:git_touch/scaffolds/refresh_stateful.dart'; +import 'package:git_touch/utils/utils.dart'; +import 'package:git_touch/widgets/action_button.dart'; +import 'package:git_touch/widgets/action_entry.dart'; +import 'package:git_touch/widgets/avatar.dart'; +import 'package:git_touch/widgets/link.dart'; +import 'package:git_touch/widgets/comment_item.dart'; +import 'package:primer/primer.dart'; +import 'package:provider/provider.dart'; +import 'package:git_touch/models/auth.dart'; +import 'package:git_touch/models/theme.dart'; +import 'package:tuple/tuple.dart'; + +class GtIssueScreen extends StatelessWidget { + final String owner; + final String name; + final String number; + final bool isPr; + + GtIssueScreen(this.owner, this.name, this.number, {this.isPr: false}); + + List _buildCommentActionItem( + BuildContext context, GiteaComment comment) { + final auth = context.read(); + final theme = context.read(); + return [ + ActionItem( + iconData: Octicons.pencil, + text: 'Edit', + onTap: (_) { + final uri = Uri( + path: '/gitea/$owner/$name/issues/$number/comment', + queryParameters: { + 'body': comment.body, + 'id': comment.id.toString(), + }, + ).toString(); + theme.push(context, uri); + }), + ActionItem( + iconData: Octicons.trashcan, + text: 'Delete', + onTap: (_) async { + await auth.fetchGitea( + '/repos/$owner/$name/issues/comments/${comment.id}', + requestType: 'DELETE'); + await theme.push(context, '/gitea/$owner/$name/issues/$number', + replace: true); + }, + ), + ]; + } + + @override + Widget build(BuildContext context) { + return RefreshStatefulScaffold>>( + title: Text("Issue: #$number"), + fetch: () async { + final auth = context.read(); + final items = await Future.wait([ + auth.fetchGitea('/repos/$owner/$name/issues/$number'), + auth.fetchGitea('/repos/$owner/$name/issues/$number/comments') + ]); + return Tuple2(GiteaIssue.fromJson(items[0]), + [for (var v in items[1]) GiteaComment.fromJson(v)]); + }, + actionBuilder: (data, _) => ActionEntry( + iconData: Octicons.plus, + url: '/gitea/$owner/$name/issues/$number/comment', + ), + bodyBuilder: (data, _) { + final issue = data.item1; + final comments = data.item2; + final theme = context.read(); + final auth = context.read(); + return Column(children: [ + Container( + padding: CommonStyle.padding, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Link( + url: '/gitea/$owner/$name', + child: Row( + children: [ + Avatar( + url: issue.user.avatarUrl, + size: AvatarSize.extraSmall, + ), + SizedBox(width: 4), + Text( + '$owner / $name', + style: TextStyle( + fontSize: 17, + color: theme.palette.secondaryText, + ), + ), + SizedBox(width: 4), + Text( + '#$number', + style: TextStyle( + fontSize: 17, + color: theme.palette.tertiaryText, + ), + ), + ], + ), + ), + SizedBox(height: 8), + Text( + issue.title, + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.w600, + ), + ), + SizedBox(height: 8), + StateLabel( + issue.state == 'open' + ? StateLabelStatus.issueOpened + : StateLabelStatus.issueClosed, + small: true), + SizedBox(height: 16), + CommonStyle.border, + ], + )), + Column(children: [ + for (var comment in comments) ...[ + Padding( + padding: EdgeInsets.only(left: 10), + child: CommentItem( + avatar: Avatar( + url: comment.user.avatarUrl, + linkUrl: '/gitea/${comment.user.login}', + ), + createdAt: comment.createdAt, + body: comment.body, + login: comment.user.login, + prefix: 'gitea', + commentActionItemList: + _buildCommentActionItem(context, comment), + )), + CommonStyle.border, + SizedBox(height: 16), + ], + ]), + ]); + }, + ); + } +} diff --git a/lib/screens/gt_issue_comment.dart b/lib/screens/gt_issue_comment.dart new file mode 100644 index 0000000..20c5589 --- /dev/null +++ b/lib/screens/gt_issue_comment.dart @@ -0,0 +1,81 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:git_touch/models/auth.dart'; +import 'package:git_touch/models/theme.dart'; +import 'package:git_touch/scaffolds/common.dart'; +import 'package:git_touch/utils/utils.dart'; +import 'package:provider/provider.dart'; + +class GtIssueCommentScreen extends StatefulWidget { + final String owner; + final String name; + final String number; + final bool isPr; + final String body; + final String id; + GtIssueCommentScreen(this.owner, this.name, this.number, + {this.isPr: false, this.body: '', this.id: ''}); + + @override + _GtIssueCommentScreenState createState() => _GtIssueCommentScreenState(); +} + +class _GtIssueCommentScreenState extends State { + bool isEdit = false; + TextEditingController _controller = new TextEditingController(); + + @override + void initState() { + super.initState(); + _controller.text = widget.body; + if (_controller.text != '') { + isEdit = true; + } + } + + @override + Widget build(BuildContext context) { + final theme = Provider.of(context); + final auth = Provider.of(context); + return CommonScaffold( + title: Text(isEdit ? 'Update Comment' : 'New Comment'), + body: Column( + children: [ + Padding( + padding: CommonStyle.padding, + child: CupertinoTextField( + controller: _controller, + style: TextStyle(color: theme.palette.text), + placeholder: 'Body', + maxLines: 10, + ), + ), + CupertinoButton.filled( + child: Text('Comment'), + onPressed: () async { + if (!isEdit) { + final res = await auth.fetchGitea( + '/repos/${widget.owner}/${widget.name}/${widget.isPr ? 'pulls' : 'issues'}/${widget.number}/comments', + requestType: 'POST', + body: {'body': _controller.text, 'repo': widget.name}, + ); + } else { + final res = await auth.fetchGitea( + '/repos/${widget.owner}/${widget.name}/${widget.isPr ? 'pulls' : 'issues'}/comments/${int.parse(widget.id)}', + requestType: 'PATCH', + body: {'body': _controller.text, 'repo': widget.name}, + ); + } + Navigator.pop(context, ''); + await theme.push( + context, + '/gitea/${widget.owner}/${widget.name}/${widget.isPr ? 'pulls' : 'issues'}/${widget.number}', + replace: true, + ); + }, + ), + ], + ), + ); + } +} diff --git a/lib/screens/gt_issue_form.dart b/lib/screens/gt_issue_form.dart new file mode 100644 index 0000000..ce5ca04 --- /dev/null +++ b/lib/screens/gt_issue_form.dart @@ -0,0 +1,78 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:git_touch/models/auth.dart'; +import 'package:git_touch/models/gitea.dart'; +import 'package:git_touch/models/theme.dart'; +import 'package:git_touch/scaffolds/common.dart'; +import 'package:git_touch/utils/utils.dart'; +import 'package:provider/provider.dart'; + +class GtIssueFormScreen extends StatefulWidget { + final String owner; + final String name; + GtIssueFormScreen(this.owner, this.name); + + @override + _GtIssueFormScreenState createState() => _GtIssueFormScreenState(); +} + +class _GtIssueFormScreenState extends State { + var _title = ''; + var _body = ''; + + @override + Widget build(BuildContext context) { + final theme = Provider.of(context); + final auth = Provider.of(context); + return CommonScaffold( + title: Text('Submit an issue'), + body: Column( + children: [ + Padding( + padding: CommonStyle.padding, + child: CupertinoTextField( + style: TextStyle(color: theme.palette.text), + placeholder: 'Title', + onChanged: (v) { + setState(() { + _title = v; + }); + }, + ), + ), + Padding( + padding: CommonStyle.padding, + child: CupertinoTextField( + style: TextStyle(color: theme.palette.text), + placeholder: 'Body', + onChanged: (v) { + setState(() { + _body = v; + }); + }, + maxLines: 10, + ), + ), + CupertinoButton.filled( + child: Text('Submit'), + onPressed: () async { + final res = await auth.fetchGitea( + '/repos/${widget.owner}/${widget.name}/issues', + requestType: 'POST', + body: {'body': _body, 'title': _title}, + ).then((v) { + return GiteaIssue.fromJson(v); + }); + Navigator.pop(context); + await theme.push( + context, + '/gitea/${widget.owner}/${widget.name}/issues', + replace: true, + ); + }, + ), + ], + ), + ); + } +} diff --git a/lib/screens/gt_issues.dart b/lib/screens/gt_issues.dart index d28ae10..51a8ff6 100644 --- a/lib/screens/gt_issues.dart +++ b/lib/screens/gt_issues.dart @@ -3,8 +3,11 @@ import 'package:git_touch/generated/l10n.dart'; import 'package:git_touch/models/auth.dart'; import 'package:git_touch/models/gitea.dart'; import 'package:git_touch/scaffolds/list_stateful.dart'; +import 'package:git_touch/utils/utils.dart'; +import 'package:git_touch/widgets/action_entry.dart'; import 'package:git_touch/widgets/app_bar_title.dart'; import 'package:git_touch/widgets/issue_item.dart'; +import 'package:git_touch/widgets/label.dart'; import 'package:provider/provider.dart'; class GtIssuesScreen extends StatelessWidget { @@ -18,7 +21,6 @@ class GtIssuesScreen extends StatelessWidget { return ListStatefulScaffold( title: AppBarTitle(isPr ? S.of(context).pullRequests : S.of(context).issues), - // TODO: create issue fetch: (page) async { final type = isPr ? 'pulls' : 'issues'; final res = await context.read().fetchGiteaWithPage( @@ -30,6 +32,12 @@ class GtIssuesScreen extends StatelessWidget { items: (res.data as List).map((v) => GiteaIssue.fromJson(v)).toList(), ); }, + actionBuilder: () => ActionEntry( + iconData: isPr ? null : Octicons.plus, + url: isPr + ? '/gitea/$owner/$name/pulls/new' + : '/gitea/$owner/$name/issues/new', + ), itemBuilder: (p) => IssueItem( author: p.user.login, avatarUrl: p.user.avatarUrl, @@ -37,7 +45,17 @@ class GtIssuesScreen extends StatelessWidget { subtitle: '#' + p.number.toString(), title: p.title, updatedAt: p.updatedAt, - url: p.htmlUrl, + url: isPr + ? p.htmlUrl // TODO: PR endpoints are not complete in Gitea + : '/gitea/$owner/$name/issues/${p.number}', + labels: isPr + ? null + : p.labels.isEmpty + ? null + : Wrap(spacing: 4, runSpacing: 4, children: [ + for (var label in p.labels) + MyLabel(name: label.name, cssColor: label.color) + ]), ), ); }