diff --git a/Flawless-Version-Control.sln.DotSettings.user b/Flawless-Version-Control.sln.DotSettings.user
index 8361805..a206e54 100644
--- a/Flawless-Version-Control.sln.DotSettings.user
+++ b/Flawless-Version-Control.sln.DotSettings.user
@@ -19,6 +19,7 @@
ForceIncluded
ForceIncluded
ForceIncluded
+ ForceIncluded
ForceIncluded
ForceIncluded
ForceIncluded
diff --git a/Flawless.Client/Flawless.Client.csproj b/Flawless.Client/Flawless.Client.csproj
index f29b58b..ff3668f 100644
--- a/Flawless.Client/Flawless.Client.csproj
+++ b/Flawless.Client/Flawless.Client.csproj
@@ -35,6 +35,10 @@
runtime; build; native; contentfiles; analyzers; buildtransitive
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
diff --git a/Flawless.Client/Models/RepositoryModel.cs b/Flawless.Client/Models/RepositoryModel.cs
index e3d7cb2..88164b6 100644
--- a/Flawless.Client/Models/RepositoryModel.cs
+++ b/Flawless.Client/Models/RepositoryModel.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.ObjectModel;
+using DynamicData.Binding;
using Flawless.Client.Remote;
using ReactiveUI.SourceGenerators;
@@ -30,10 +31,13 @@ public partial class RepositoryModel : ReactiveModel
public ObservableCollection Commits { get; } = new();
+ // todo cache depots?
public ObservableCollection Depots { get; } = new();
-
+
public ObservableCollection Locks { get; } = new();
+ public ObservableCollection Issues { get; } = new();
+
public enum RepositoryRole : byte
{
Guest = 0,
@@ -42,6 +46,40 @@ public partial class RepositoryModel : ReactiveModel
Owner = 3,
}
+ public partial class Issue : ReactiveModel
+ {
+ [Reactive] private ulong _id;
+
+ [Reactive] private string _title;
+
+ [Reactive] private string? _description;
+
+ [Reactive] private string _author;
+
+ [Reactive] private DateTime _createdAt;
+
+ [Reactive] private DateTime? _lastUpdatedAt;
+
+ [Reactive] private bool _closed;
+
+ public ObservableCollection Tags { get; } = new();
+
+ public ObservableCollection Comments { get; } = new();
+ }
+
+ public partial class Comment : ReactiveModel
+ {
+ [Reactive] private ulong _id;
+
+ [Reactive] private string _content;
+
+ [Reactive] private string _author;
+
+ [Reactive] private DateTime _createdAt;
+
+ [Reactive] private ulong? _replyTo;
+ }
+
public partial class Member : ReactiveModel
{
[Reactive] private string _username;
diff --git a/Flawless.Client/Service/Remote_Generated.cs b/Flawless.Client/Service/Remote_Generated.cs
index 3eec735..983af6c 100644
--- a/Flawless.Client/Service/Remote_Generated.cs
+++ b/Flawless.Client/Service/Remote_Generated.cs
@@ -7,6 +7,7 @@ using Refit;
using System.Collections.Generic;
using System.IO;
using System.Text.Json.Serialization;
+using System.Threading;
using System.Threading.Tasks;
using Flawless.Client.Models;
@@ -14,99 +15,148 @@ using Flawless.Client.Models;
namespace Flawless.Client.Remote
{
- [System.CodeDom.Compiler.GeneratedCode("Refitter", "1.5.2.0")]
+ [System.CodeDom.Compiler.GeneratedCode("Refitter", "1.5.3.0")]
public partial interface IFlawlessServer
{
/// A that completes when the request is finished.
/// Thrown when the request returns a non-success status code.
[Post("/api/admin/user/delete/{username}")]
- Task Delete(string username);
+ Task Delete(string username, CancellationToken cancellationToken = default);
/// A that completes when the request is finished.
/// Thrown when the request returns a non-success status code.
[Post("/api/admin/user/enable/{username}")]
- Task Enable(string username);
+ Task Enable(string username, CancellationToken cancellationToken = default);
/// A that completes when the request is finished.
/// Thrown when the request returns a non-success status code.
[Post("/api/admin/user/disable/{username}")]
- Task Disable(string username);
+ Task Disable(string username, CancellationToken cancellationToken = default);
/// A that completes when the request is finished.
/// Thrown when the request returns a non-success status code.
+ [Headers("Content-Type: application/json")]
[Post("/api/admin/user/reset_password")]
- Task ResetPassword([Body] ResetPasswordRequest body);
+ Task ResetPassword([Body] ResetPasswordRequest body, CancellationToken cancellationToken = default);
/// OK
/// Thrown when the request returns a non-success status code.
[Headers("Accept: text/plain, application/json, text/json")]
[Get("/api/auth/status")]
- Task Status();
+ Task Status(CancellationToken cancellationToken = default);
/// A that completes when the request is finished.
/// Thrown when the request returns a non-success status code.
+ [Headers("Content-Type: application/json")]
[Post("/api/auth/register")]
- Task Register([Body] RegisterRequest body);
+ Task Register([Body] RegisterRequest body, CancellationToken cancellationToken = default);
/// OK
/// Thrown when the request returns a non-success status code.
- [Headers("Accept: text/plain, application/json, text/json")]
+ [Headers("Accept: text/plain, application/json, text/json", "Content-Type: application/json")]
[Post("/api/auth/login")]
- Task Login([Body] LoginRequest body);
+ Task Login([Body] LoginRequest body, CancellationToken cancellationToken = default);
/// OK
/// Thrown when the request returns a non-success status code.
- [Headers("Accept: text/plain, application/json, text/json")]
+ [Headers("Accept: text/plain, application/json, text/json", "Content-Type: application/json")]
[Post("/api/auth/refresh")]
- Task Refresh([Body] TokenInfo body);
+ Task Refresh([Body] TokenInfo body, CancellationToken cancellationToken = default);
/// A that completes when the request is finished.
/// Thrown when the request returns a non-success status code.
[Post("/api/auth/logout_all")]
- Task LogoutAll();
+ Task LogoutAll(CancellationToken cancellationToken = default);
/// A that completes when the request is finished.
/// Thrown when the request returns a non-success status code.
+ [Headers("Content-Type: application/json")]
[Post("/api/auth/renew_password")]
- Task RenewPassword([Body] ResetPasswordRequest body);
+ Task RenewPassword([Body] ResetPasswordRequest body, CancellationToken cancellationToken = default);
/// A that completes when the request is finished.
/// Thrown when the request returns a non-success status code.
[Get("/")]
- Task Index();
+ Task Index(CancellationToken cancellationToken = default);
+
+ /// OK
+ /// Thrown when the request returns a non-success status code.
+ [Headers("Accept: text/plain, application/json, text/json", "Content-Type: application/json")]
+ [Post("/api/issue/{userName}/{repositoryName}/create")]
+ Task Create(string userName, string repositoryName, [Body] CreateIssueRequest body, CancellationToken cancellationToken = default);
+
+ /// A that completes when the request is finished.
+ /// Thrown when the request returns a non-success status code.
+ [Headers("Content-Type: application/json")]
+ [Post("/api/issue/{userName}/{repositoryName}/{issueId}/comment")]
+ Task Comment(string userName, string repositoryName, long issueId, [Body] AddCommentRequest body, CancellationToken cancellationToken = default);
+
+ /// A that completes when the request is finished.
+ /// Thrown when the request returns a non-success status code.
+ [Post("/api/issue/{userName}/{repositoryName}/{issueId}/close")]
+ Task Close(string userName, string repositoryName, long issueId, CancellationToken cancellationToken = default);
+
+ /// OK
+ /// Thrown when the request returns a non-success status code.
+ [Headers("Accept: text/plain, application/json, text/json")]
+ [Get("/api/issue/{userName}/{repositoryName}/list")]
+ Task List(string userName, string repositoryName, CancellationToken cancellationToken = default);
+
+ /// OK
+ /// Thrown when the request returns a non-success status code.
+ [Headers("Accept: text/plain, application/json, text/json")]
+ [Get("/api/issue/{userName}/{repositoryName}/{issueId}")]
+ Task Issue(string userName, string repositoryName, long issueId, CancellationToken cancellationToken = default);
+
+ /// A that completes when the request is finished.
+ /// Thrown when the request returns a non-success status code.
+ [Headers("Content-Type: application/json")]
+ [Patch("/api/issue/{userName}/{repositoryName}/{issueId}/edit")]
+ Task Edit(string userName, string repositoryName, long issueId, [Body] UpdateIssueRequest body, CancellationToken cancellationToken = default);
+
+ /// A that completes when the request is finished.
+ /// Thrown when the request returns a non-success status code.
+ [Post("/api/issue/{userName}/{repositoryName}/{issueId}/reopen")]
+ Task Reopen(string userName, string repositoryName, long issueId, CancellationToken cancellationToken = default);
+
+ /// OK
+ /// Thrown when the request returns a non-success status code.
+ [Headers("Accept: text/plain, application/json, text/json")]
+ [Get("/api/issue/{userName}/{repositoryName}/{issueId}/comments")]
+ Task Comments(string userName, string repositoryName, long issueId, CancellationToken cancellationToken = default);
/// A that completes when the request is finished.
/// Thrown when the request returns a non-success status code.
[Post("/api/repo/{userName}/{repositoryName}/delete_repo")]
- Task DeleteRepo(string repositoryName, string userName);
+ Task DeleteRepo(string userName, string repositoryName, CancellationToken cancellationToken = default);
/// OK
/// Thrown when the request returns a non-success status code.
[Headers("Accept: text/plain, application/json, text/json")]
[Get("/api/repo/{userName}/{repositoryName}/get_info")]
- Task GetInfo(string repositoryName, string userName);
+ Task GetInfo(string userName, string repositoryName, CancellationToken cancellationToken = default);
/// A that completes when the request is finished.
/// Thrown when the request returns a non-success status code.
[Post("/api/repo/{userName}/{repositoryName}/archive_repo")]
- Task ArchiveRepo(string repositoryName, string userName);
+ Task ArchiveRepo(string userName, string repositoryName, CancellationToken cancellationToken = default);
/// A that completes when the request is finished.
/// Thrown when the request returns a non-success status code.
[Post("/api/repo/{userName}/{repositoryName}/unarchive_repo")]
- Task UnarchiveRepo(string repositoryName, string userName);
+ Task UnarchiveRepo(string userName, string repositoryName, CancellationToken cancellationToken = default);
/// OK
/// Thrown when the request returns a non-success status code.
[Headers("Accept: text/plain, application/json, text/json")]
[Get("/api/repo/{userName}/{repositoryName}/get_users")]
- Task GetUsers(string repositoryName, string userName);
+ Task GetUsers(string userName, string repositoryName, CancellationToken cancellationToken = default);
/// OK
/// Thrown when the request returns a non-success status code.
[Headers("Accept: text/plain, application/json, text/json")]
[Post("/api/repo/{userName}/{repositoryName}/fetch_manifest")]
- Task FetchManifest(string userName, string repositoryName, [Query] string commitId);
+ Task FetchManifest(string userName, string repositoryName, [Query] string commitId, CancellationToken cancellationToken = default);
/// OK
/// Thrown when the request returns a non-success status code.
@@ -118,48 +168,48 @@ namespace Flawless.Client.Remote
/// Thrown when the request returns a non-success status code.
[Headers("Accept: text/plain, application/json, text/json")]
[Post("/api/repo/{userName}/{repositoryName}/list_commit")]
- Task ListCommit(string userName, string repositoryName);
+ Task ListCommit(string userName, string repositoryName, CancellationToken cancellationToken = default);
/// OK
/// Thrown when the request returns a non-success status code.
[Headers("Accept: text/plain, application/json, text/json")]
[Post("/api/repo/{userName}/{repositoryName}/list_locked_files")]
- Task ListLockedFiles(string userName, string repositoryName);
+ Task ListLockedFiles(string userName, string repositoryName, CancellationToken cancellationToken = default);
/// OK
/// Thrown when the request returns a non-success status code.
[Headers("Accept: text/plain, application/json, text/json")]
[Get("/api/repo/{userName}/{repositoryName}/peek_commit")]
- Task PeekCommit(string userName, string repositoryName);
+ Task PeekCommit(string userName, string repositoryName, CancellationToken cancellationToken = default);
/// A that completes when the request is finished.
/// Thrown when the request returns a non-success status code.
[Post("/api/repo/{userName}/{repositoryName}/lock_file")]
- Task LockFile(string userName, string repositoryName, [Query] string path);
+ Task LockFile(string userName, string repositoryName, [Query] string path, CancellationToken cancellationToken = default);
/// A that completes when the request is finished.
/// Thrown when the request returns a non-success status code.
[Post("/api/repo/{userName}/{repositoryName}/unlock_file")]
- Task UnlockFile(string userName, string repositoryName, [Query] string path);
+ Task UnlockFile(string userName, string repositoryName, [Query] string path, CancellationToken cancellationToken = default);
/// OK
/// Thrown when the request returns a non-success status code.
[Multipart]
- [Headers("Accept: text/plain, application/json, text/json")]
+ [Headers("Accept: text/plain, application/json, text/json", "Content-Type: multipart/form-data")]
[Post("/api/repo/{userName}/{repositoryName}/create_commit")]
- Task CreateCommit(string userName, string repositoryName, StreamPart depot, string message, IEnumerable workspaceSnapshot, IEnumerable requiredDepots, string mainDepotId);
+ Task CreateCommit(string userName, string repositoryName, StreamPart depot, string message, IEnumerable workspaceSnapshot, IEnumerable requiredDepots, string mainDepotId, CancellationToken cancellationToken = default);
/// OK
/// Thrown when the request returns a non-success status code.
[Headers("Accept: text/plain, application/json, text/json")]
[Get("/api/repo_list")]
- Task RepoList();
+ Task RepoList(CancellationToken cancellationToken = default);
/// OK
/// Thrown when the request returns a non-success status code.
[Headers("Accept: text/plain, application/json, text/json")]
[Post("/api/repo_create")]
- Task RepoCreate([Query] string repositoryName, [Query] string description);
+ Task RepoCreate([Query] string repositoryName, [Query] string description, CancellationToken cancellationToken = default);
/// A that completes when the request is finished.
/// Thrown when the request returns a non-success status code.
@@ -169,39 +219,42 @@ namespace Flawless.Client.Remote
/// A that completes when the request is finished.
/// Thrown when the request returns a non-success status code.
[Post("/api/delete_user")]
- Task DeleteUser([Query] string repositoryName, [Query] string delUser);
+ Task DeleteUser([Query] string repositoryName, [Query] string delUser, CancellationToken cancellationToken = default);
/// A that completes when the request is finished.
/// Thrown when the request returns a non-success status code.
+ [Headers("Content-Type: application/json")]
[Post("/api/user/update_info")]
- Task UpdateInfo([Body] UserInfoModifyResponse body);
+ Task UpdateInfo([Body] UserInfoModifyResponse body, CancellationToken cancellationToken = default);
/// A that completes when the request is finished.
/// Thrown when the request returns a non-success status code.
+ [Headers("Content-Type: application/json")]
[Post("/api/user/update_email")]
- Task UpdateEmail([Body] UserContactModifyResponse body);
+ Task UpdateEmail([Body] UserContactModifyResponse body, CancellationToken cancellationToken = default);
/// A that completes when the request is finished.
/// Thrown when the request returns a non-success status code.
+ [Headers("Content-Type: application/json")]
[Post("/api/user/update_phone")]
- Task UpdatePhone([Body] UserContactModifyResponse body);
+ Task UpdatePhone([Body] UserContactModifyResponse body, CancellationToken cancellationToken = default);
/// OK
/// Thrown when the request returns a non-success status code.
[Headers("Accept: text/plain, application/json, text/json")]
[Get("/api/user/get_info")]
- Task GetInfo([Query] string username);
+ Task GetInfo([Query] string username, CancellationToken cancellationToken = default);
/// OK
/// Thrown when the request returns a non-success status code.
[Headers("Accept: text/plain, application/json, text/json")]
[Get("/api/user/query_info")]
- Task QueryInfo([Query] string keyword);
+ Task QueryInfo([Query] string keyword, CancellationToken cancellationToken = default);
/// A that completes when the request is finished.
/// Thrown when the request returns a non-success status code.
[Get("/api/user/delete")]
- Task Delete();
+ Task Delete(CancellationToken cancellationToken = default);
}
@@ -210,7 +263,7 @@ namespace Flawless.Client.Remote
//----------------------
//
-// Generated using the NSwag toolchain v14.2.0.0 (NJsonSchema v11.1.0.0 (Newtonsoft.Json v13.0.0.0)) (http://NSwag.org)
+// Generated using the NSwag toolchain v14.3.0.0 (NJsonSchema v11.2.0.0 (Newtonsoft.Json v13.0.0.0)) (http://NSwag.org)
//
//----------------------
@@ -223,6 +276,8 @@ namespace Flawless.Client.Remote
#pragma warning disable 1591 // Disable "CS1591 Missing XML comment for publicly visible type or member ..."
#pragma warning disable 8073 // Disable "CS8073 The result of the expression is always 'false' since a value of type 'T' is never equal to 'null' of type 'T?'"
#pragma warning disable 3016 // Disable "CS3016 Arrays as attribute arguments is not CLS-compliant"
+#pragma warning disable 8600 // Disable "CS8600 Converting null literal or possible null value to non-nullable type"
+#pragma warning disable 8602 // Disable "CS8602 Dereference of a possibly null reference"
#pragma warning disable 8603 // Disable "CS8603 Possible null reference return"
#pragma warning disable 8604 // Disable "CS8604 Possible null reference argument for parameter"
#pragma warning disable 8625 // Disable "CS8625 Cannot convert null literal to non-nullable reference type"
@@ -234,7 +289,49 @@ namespace Flawless.Client.Remote
- [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.2.0.0 (NJsonSchema v11.1.0.0 (Newtonsoft.Json v13.0.0.0))")]
+ [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.3.0.0 (NJsonSchema v11.2.0.0 (Newtonsoft.Json v13.0.0.0))")]
+ public partial class AddCommentRequest
+ {
+
+ [JsonPropertyName("content")]
+ public string Content { get; set; }
+
+ [JsonPropertyName("replyTo")]
+ public long? ReplyTo { get; set; }
+
+ }
+
+ [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.3.0.0 (NJsonSchema v11.2.0.0 (Newtonsoft.Json v13.0.0.0))")]
+ public partial class CommentResponse
+ {
+
+ [JsonPropertyName("commentId")]
+ public long CommentId { get; set; }
+
+ [JsonPropertyName("author")]
+ public string Author { get; set; }
+
+ [JsonPropertyName("content")]
+ public string Content { get; set; }
+
+ [JsonPropertyName("createdAt")]
+ public System.DateTimeOffset CreatedAt { get; set; }
+
+ [JsonPropertyName("replyToId")]
+ public long? ReplyToId { get; set; }
+
+ }
+
+ [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.3.0.0 (NJsonSchema v11.2.0.0 (Newtonsoft.Json v13.0.0.0))")]
+ public partial class CommentResponseListingResponse
+ {
+
+ [JsonPropertyName("result")]
+ public ICollection Result { get; set; }
+
+ }
+
+ [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.3.0.0 (NJsonSchema v11.2.0.0 (Newtonsoft.Json v13.0.0.0))")]
public partial class CommitManifest
{
@@ -249,7 +346,7 @@ namespace Flawless.Client.Remote
}
- [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.2.0.0 (NJsonSchema v11.1.0.0 (Newtonsoft.Json v13.0.0.0))")]
+ [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.3.0.0 (NJsonSchema v11.2.0.0 (Newtonsoft.Json v13.0.0.0))")]
public partial class CommitSuccessResponse
{
@@ -264,7 +361,22 @@ namespace Flawless.Client.Remote
}
- [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.2.0.0 (NJsonSchema v11.1.0.0 (Newtonsoft.Json v13.0.0.0))")]
+ [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.3.0.0 (NJsonSchema v11.2.0.0 (Newtonsoft.Json v13.0.0.0))")]
+ public partial class CreateIssueRequest
+ {
+
+ [JsonPropertyName("title")]
+ public string Title { get; set; }
+
+ [JsonPropertyName("description")]
+ public string Description { get; set; }
+
+ [JsonPropertyName("tag")]
+ public string? Tag { get; set; }
+
+ }
+
+ [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.3.0.0 (NJsonSchema v11.2.0.0 (Newtonsoft.Json v13.0.0.0))")]
public partial class DepotLabel
{
@@ -276,7 +388,7 @@ namespace Flawless.Client.Remote
}
- [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.2.0.0 (NJsonSchema v11.1.0.0 (Newtonsoft.Json v13.0.0.0))")]
+ [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.3.0.0 (NJsonSchema v11.2.0.0 (Newtonsoft.Json v13.0.0.0))")]
public partial class GuidPeekResponse
{
@@ -285,7 +397,73 @@ namespace Flawless.Client.Remote
}
- [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.2.0.0 (NJsonSchema v11.1.0.0 (Newtonsoft.Json v13.0.0.0))")]
+ [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.3.0.0 (NJsonSchema v11.2.0.0 (Newtonsoft.Json v13.0.0.0))")]
+ public partial class IssueDetailInfo
+ {
+
+ [JsonPropertyName("id")]
+ public long Id { get; set; }
+
+ [JsonPropertyName("author")]
+ public string Author { get; set; }
+
+ [JsonPropertyName("title")]
+ public string Title { get; set; }
+
+ [JsonPropertyName("description")]
+ public string Description { get; set; }
+
+ [JsonPropertyName("createAt")]
+ public System.DateTimeOffset CreateAt { get; set; }
+
+ [JsonPropertyName("lastUpdate")]
+ public System.DateTimeOffset LastUpdate { get; set; }
+
+ [JsonPropertyName("closed")]
+ public bool Closed { get; set; }
+
+ [JsonPropertyName("tag")]
+ public string Tag { get; set; }
+
+ }
+
+ [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.3.0.0 (NJsonSchema v11.2.0.0 (Newtonsoft.Json v13.0.0.0))")]
+ public partial class IssueInfo
+ {
+
+ [JsonPropertyName("id")]
+ public long Id { get; set; }
+
+ [JsonPropertyName("author")]
+ public string Author { get; set; }
+
+ [JsonPropertyName("title")]
+ public string Title { get; set; }
+
+ [JsonPropertyName("createAt")]
+ public System.DateTimeOffset CreateAt { get; set; }
+
+ [JsonPropertyName("lastUpdate")]
+ public System.DateTimeOffset LastUpdate { get; set; }
+
+ [JsonPropertyName("closed")]
+ public bool Closed { get; set; }
+
+ [JsonPropertyName("tag")]
+ public string Tag { get; set; }
+
+ }
+
+ [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.3.0.0 (NJsonSchema v11.2.0.0 (Newtonsoft.Json v13.0.0.0))")]
+ public partial class IssueInfoListingResponse
+ {
+
+ [JsonPropertyName("result")]
+ public ICollection Result { get; set; }
+
+ }
+
+ [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.3.0.0 (NJsonSchema v11.2.0.0 (Newtonsoft.Json v13.0.0.0))")]
public partial class LockFileInfo
{
@@ -297,7 +475,7 @@ namespace Flawless.Client.Remote
}
- [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.2.0.0 (NJsonSchema v11.1.0.0 (Newtonsoft.Json v13.0.0.0))")]
+ [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.3.0.0 (NJsonSchema v11.2.0.0 (Newtonsoft.Json v13.0.0.0))")]
public partial class LockFileInfoListingResponse
{
@@ -306,7 +484,7 @@ namespace Flawless.Client.Remote
}
- [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.2.0.0 (NJsonSchema v11.1.0.0 (Newtonsoft.Json v13.0.0.0))")]
+ [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.3.0.0 (NJsonSchema v11.2.0.0 (Newtonsoft.Json v13.0.0.0))")]
public partial class LoginRequest
{
@@ -320,7 +498,7 @@ namespace Flawless.Client.Remote
}
- [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.2.0.0 (NJsonSchema v11.1.0.0 (Newtonsoft.Json v13.0.0.0))")]
+ [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.3.0.0 (NJsonSchema v11.2.0.0 (Newtonsoft.Json v13.0.0.0))")]
public partial class RegisterRequest
{
@@ -338,7 +516,7 @@ namespace Flawless.Client.Remote
}
- [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.2.0.0 (NJsonSchema v11.1.0.0 (Newtonsoft.Json v13.0.0.0))")]
+ [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.3.0.0 (NJsonSchema v11.2.0.0 (Newtonsoft.Json v13.0.0.0))")]
public partial class RepoUserRole
{
@@ -351,7 +529,7 @@ namespace Flawless.Client.Remote
}
- [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.2.0.0 (NJsonSchema v11.1.0.0 (Newtonsoft.Json v13.0.0.0))")]
+ [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.3.0.0 (NJsonSchema v11.2.0.0 (Newtonsoft.Json v13.0.0.0))")]
public partial class RepoUserRoleListingResponse
{
@@ -360,7 +538,7 @@ namespace Flawless.Client.Remote
}
- [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.2.0.0 (NJsonSchema v11.1.0.0 (Newtonsoft.Json v13.0.0.0))")]
+ [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.3.0.0 (NJsonSchema v11.2.0.0 (Newtonsoft.Json v13.0.0.0))")]
public partial class RepositoryCommitResponse
{
@@ -381,7 +559,7 @@ namespace Flawless.Client.Remote
}
- [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.2.0.0 (NJsonSchema v11.1.0.0 (Newtonsoft.Json v13.0.0.0))")]
+ [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.3.0.0 (NJsonSchema v11.2.0.0 (Newtonsoft.Json v13.0.0.0))")]
public partial class RepositoryCommitResponseListingResponse
{
@@ -390,7 +568,7 @@ namespace Flawless.Client.Remote
}
- [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.2.0.0 (NJsonSchema v11.1.0.0 (Newtonsoft.Json v13.0.0.0))")]
+ [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.3.0.0 (NJsonSchema v11.2.0.0 (Newtonsoft.Json v13.0.0.0))")]
public partial class RepositoryInfoResponse
{
@@ -413,7 +591,7 @@ namespace Flawless.Client.Remote
}
- [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.2.0.0 (NJsonSchema v11.1.0.0 (Newtonsoft.Json v13.0.0.0))")]
+ [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.3.0.0 (NJsonSchema v11.2.0.0 (Newtonsoft.Json v13.0.0.0))")]
public partial class RepositoryInfoResponseListingResponse
{
@@ -422,7 +600,7 @@ namespace Flawless.Client.Remote
}
- [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.2.0.0 (NJsonSchema v11.1.0.0 (Newtonsoft.Json v13.0.0.0))")]
+ [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.3.0.0 (NJsonSchema v11.2.0.0 (Newtonsoft.Json v13.0.0.0))")]
public enum RepositoryRole
{
@@ -436,7 +614,7 @@ namespace Flawless.Client.Remote
}
- [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.2.0.0 (NJsonSchema v11.1.0.0 (Newtonsoft.Json v13.0.0.0))")]
+ [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.3.0.0 (NJsonSchema v11.2.0.0 (Newtonsoft.Json v13.0.0.0))")]
public partial class ResetPasswordRequest
{
@@ -452,7 +630,7 @@ namespace Flawless.Client.Remote
}
- [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.2.0.0 (NJsonSchema v11.1.0.0 (Newtonsoft.Json v13.0.0.0))")]
+ [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.3.0.0 (NJsonSchema v11.2.0.0 (Newtonsoft.Json v13.0.0.0))")]
public partial class ServerStatusResponse
{
@@ -464,7 +642,7 @@ namespace Flawless.Client.Remote
}
- [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.2.0.0 (NJsonSchema v11.1.0.0 (Newtonsoft.Json v13.0.0.0))")]
+ [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.3.0.0 (NJsonSchema v11.2.0.0 (Newtonsoft.Json v13.0.0.0))")]
public partial class TokenInfo
{
@@ -477,7 +655,22 @@ namespace Flawless.Client.Remote
}
- [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.2.0.0 (NJsonSchema v11.1.0.0 (Newtonsoft.Json v13.0.0.0))")]
+ [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.3.0.0 (NJsonSchema v11.2.0.0 (Newtonsoft.Json v13.0.0.0))")]
+ public partial class UpdateIssueRequest
+ {
+
+ [JsonPropertyName("title")]
+ public string? Title { get; set; }
+
+ [JsonPropertyName("description")]
+ public string? Description { get; set; }
+
+ [JsonPropertyName("tag")]
+ public string? Tag { get; set; }
+
+ }
+
+ [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.3.0.0 (NJsonSchema v11.2.0.0 (Newtonsoft.Json v13.0.0.0))")]
public partial class UserContactModifyResponse
{
@@ -489,7 +682,7 @@ namespace Flawless.Client.Remote
}
- [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.2.0.0 (NJsonSchema v11.1.0.0 (Newtonsoft.Json v13.0.0.0))")]
+ [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.3.0.0 (NJsonSchema v11.2.0.0 (Newtonsoft.Json v13.0.0.0))")]
public partial class UserInfoModifyResponse
{
@@ -507,7 +700,7 @@ namespace Flawless.Client.Remote
}
- [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.2.0.0 (NJsonSchema v11.1.0.0 (Newtonsoft.Json v13.0.0.0))")]
+ [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.3.0.0 (NJsonSchema v11.2.0.0 (Newtonsoft.Json v13.0.0.0))")]
public partial class UserInfoResponse
{
@@ -540,7 +733,7 @@ namespace Flawless.Client.Remote
}
- [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.2.0.0 (NJsonSchema v11.1.0.0 (Newtonsoft.Json v13.0.0.0))")]
+ [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.3.0.0 (NJsonSchema v11.2.0.0 (Newtonsoft.Json v13.0.0.0))")]
public partial class UserInfoResponseListingResponse
{
@@ -549,7 +742,7 @@ namespace Flawless.Client.Remote
}
- [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.2.0.0 (NJsonSchema v11.1.0.0 (Newtonsoft.Json v13.0.0.0))")]
+ [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.3.0.0 (NJsonSchema v11.2.0.0 (Newtonsoft.Json v13.0.0.0))")]
public enum UserSex
{
@@ -563,7 +756,7 @@ namespace Flawless.Client.Remote
}
- [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.2.0.0 (NJsonSchema v11.1.0.0 (Newtonsoft.Json v13.0.0.0))")]
+ [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.3.0.0 (NJsonSchema v11.2.0.0 (Newtonsoft.Json v13.0.0.0))")]
public partial class WorkspaceFile
{
@@ -575,7 +768,7 @@ namespace Flawless.Client.Remote
}
- [System.CodeDom.Compiler.GeneratedCode("NSwag", "14.2.0.0 (NJsonSchema v11.1.0.0 (Newtonsoft.Json v13.0.0.0))")]
+ [System.CodeDom.Compiler.GeneratedCode("NSwag", "14.3.0.0 (NJsonSchema v11.2.0.0 (Newtonsoft.Json v13.0.0.0))")]
public partial class FileParameter
{
public FileParameter(System.IO.Stream data)
@@ -602,7 +795,7 @@ namespace Flawless.Client.Remote
public string ContentType { get; private set; }
}
- [System.CodeDom.Compiler.GeneratedCode("NSwag", "14.2.0.0 (NJsonSchema v11.1.0.0 (Newtonsoft.Json v13.0.0.0))")]
+ [System.CodeDom.Compiler.GeneratedCode("NSwag", "14.3.0.0 (NJsonSchema v11.2.0.0 (Newtonsoft.Json v13.0.0.0))")]
public partial class FileResponse : System.IDisposable
{
private System.IDisposable _client;
@@ -650,6 +843,8 @@ namespace Flawless.Client.Remote
#pragma warning restore 1591
#pragma warning restore 8073
#pragma warning restore 3016
+#pragma warning restore 8600
+#pragma warning restore 8602
#pragma warning restore 8603
#pragma warning restore 8604
#pragma warning restore 8625
\ No newline at end of file
diff --git a/Flawless.Client/Service/RepositoryService.cs b/Flawless.Client/Service/RepositoryService.cs
index 4321d53..a570fe9 100644
--- a/Flawless.Client/Service/RepositoryService.cs
+++ b/Flawless.Client/Service/RepositoryService.cs
@@ -1,10 +1,13 @@
using System;
using System.Collections.Generic;
+using System.Collections.Immutable;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
using System.Net;
+using System.Text;
using System.Threading.Tasks;
+using AvaloniaEdit.Utils;
using Flawless.Abstraction;
using Flawless.Client.Models;
using Flawless.Client.Remote;
@@ -278,6 +281,293 @@ public class RepositoryService : BaseService
return false;
}
}
+
+ public async ValueTask UpdateIssuesListFromServerAsync(RepositoryModel repo)
+ {
+ var api = Api.C;
+ try
+ {
+ if (api.RequireRefreshToken() && !(await api.TryRefreshTokenAsync()))
+ {
+ api.ClearGateway();
+ return false;
+ }
+
+ var issues = (await api.Gateway.List(repo.Name, repo.OwnerName))
+ .Result.ToImmutableDictionary(x => (ulong) x.Id);
+
+ for (var i = 0; i < repo.Issues.Count; i++)
+ {
+ if (!issues.ContainsKey(repo.Issues[i].Id))
+ repo.Issues.RemoveAt(i);
+ }
+
+ foreach (var (id, info) in issues)
+ {
+ var i = repo.Issues.FirstOrDefault(x => x.Id == id);
+ var tags = info.Tag.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
+ if (i == null)
+ {
+ i = new RepositoryModel.Issue
+ {
+ Author = info.Author,
+ Id = id,
+ Title = info.Title,
+ Description = null,
+ Closed = info.Closed,
+ CreatedAt = info.CreateAt.UtcDateTime,
+ LastUpdatedAt = info.LastUpdate.UtcDateTime
+ };
+
+ i.Tags.AddRange(tags);
+ repo.Issues.Add(i);
+ }
+ else
+ {
+ i.Title = info.Title;
+ i.Closed = info.Closed;
+ i.LastUpdatedAt = info.LastUpdate.UtcDateTime;
+ i.Author = info.Author;
+
+ i.Tags.Clear();
+ i.Tags.AddRange(tags);
+ }
+ }
+
+ repo.Issues.Sort((x, y) => (int) ((long) x.Id - (long) y.Id));
+ }
+ catch (Exception e)
+ {
+ UIHelper.NotifyError(e);
+ Console.WriteLine(e);
+ return false;
+ }
+
+ return true;
+ }
+
+ public async ValueTask UpdateIssueDetailsFromServerAsync(RepositoryModel repo, ulong issueId)
+ {
+ var api = Api.C;
+ try
+ {
+ if (api.RequireRefreshToken() && !(await api.TryRefreshTokenAsync()))
+ {
+ api.ClearGateway();
+ return false;
+ }
+
+ var issue = await api.Gateway.Issue(repo.Name, repo.OwnerName, (long)issueId);
+ var details = (await api.Gateway.Comments(repo.Name, repo.OwnerName, (long)issueId))
+ .Result.ToImmutableDictionary(x => x.CommentId);
+
+ var entity = repo.Issues.FirstOrDefault(x => x.Id == issueId);
+ var tags = issue.Tag.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
+
+ if (entity == null)
+ {
+ entity = new RepositoryModel.Issue
+ {
+ Author = issue.Author,
+ Id = (ulong) issue.Id,
+ Title = issue.Title,
+ Description = null,
+ Closed = issue.Closed,
+ CreatedAt = issue.CreateAt.UtcDateTime,
+ LastUpdatedAt = issue.LastUpdate.UtcDateTime
+ };
+
+ entity.Tags.AddRange(tags);
+ repo.Issues.Add(entity);
+
+ entity.Comments.AddRange(details.Select(x => new RepositoryModel.Comment
+ {
+ Id = (ulong) x.Key,
+ Author = x.Value.Author,
+ Content = x.Value.Content,
+ CreatedAt = x.Value.CreatedAt.UtcDateTime,
+ ReplyTo = x.Value.ReplyToId.HasValue ? (ulong) x.Value.ReplyToId.Value : null
+ }));
+ }
+ else
+ {
+ entity.Title = issue.Title;
+ entity.Closed = issue.Closed;
+ entity.LastUpdatedAt = issue.LastUpdate.UtcDateTime;
+ entity.Author = issue.Author;
+
+ entity.Tags.Clear();
+ entity.Tags.AddRange(tags);
+
+ for (var i = 0; i < entity.Comments.Count; i++)
+ {
+ var c = entity.Comments[i];
+ if (!details.ContainsKey((long) c.Id)) repo.Issues.RemoveAt(i);
+ }
+
+ foreach (var (key, value) in details)
+ {
+ var d = entity.Comments.FirstOrDefault(x => x.Id == (ulong) key);
+ if (d == null)
+ {
+ var c = new RepositoryModel.Comment
+ {
+ Id = (ulong)key,
+ Author = value.Author,
+ Content = value.Content,
+ CreatedAt = value.CreatedAt.UtcDateTime,
+ ReplyTo = value.ReplyToId.HasValue ? (ulong)value.ReplyToId.Value : null
+ };
+
+ entity.Comments.Add(c);
+ }
+ else
+ {
+ d.Author = value.Author;
+ d.Content = value.Content;
+ d.CreatedAt = value.CreatedAt.UtcDateTime;
+ d.ReplyTo = value.ReplyToId.HasValue ? (ulong)value.ReplyToId.Value : null;
+ }
+ }
+ }
+
+ repo.Issues.Sort((x, y) => (int) ((long) x.Id - (long) y.Id));
+ }
+ catch (Exception e)
+ {
+ UIHelper.NotifyError(e);
+ Console.WriteLine(e);
+ return false;
+ }
+
+ return true;
+ }
+
+ public async ValueTask CreateIssueAsync(RepositoryModel repo, string title, string description, IEnumerable? tags)
+ {
+ var api = Api.C;
+ try
+ {
+ if (api.RequireRefreshToken() && !(await api.TryRefreshTokenAsync()))
+ {
+ api.ClearGateway();
+ return null;
+ }
+
+ StringBuilder? tagString = null;
+ if (tags != null)
+ {
+ new StringBuilder().AppendJoin(',', tags);
+ }
+
+ return await api.Gateway.Create(
+ repo.OwnerName,
+ repo.Name,
+ new CreateIssueRequest { Title = title, Description = description, Tag = tagString?.ToString()});
+ }
+ catch (Exception e)
+ {
+ UIHelper.NotifyError(e);
+ Console.WriteLine(e);
+ return null;
+ }
+ }
+
+ public async ValueTask CloseIssueAsync(RepositoryModel repo, long issueId)
+ {
+ var api = Api.C;
+ try
+ {
+ if (api.RequireRefreshToken() && !(await api.TryRefreshTokenAsync()))
+ {
+ api.ClearGateway();
+ return false;
+ }
+
+ await api.Gateway.Close(repo.OwnerName, repo.Name, issueId);
+ return true;
+ }
+ catch (Exception e)
+ {
+ UIHelper.NotifyError(e);
+ Console.WriteLine(e);
+ return false;
+ }
+ }
+
+ public async ValueTask ReopenIssueAsync(RepositoryModel repo, long issueId)
+ {
+ var api = Api.C;
+ try
+ {
+ if (api.RequireRefreshToken() && !(await api.TryRefreshTokenAsync()))
+ {
+ api.ClearGateway();
+ return false;
+ }
+
+ await api.Gateway.Reopen(repo.OwnerName, repo.Name, issueId);
+ return true;
+ }
+ catch (Exception e)
+ {
+ UIHelper.NotifyError(e);
+ Console.WriteLine(e);
+ return false;
+ }
+ }
+
+ public async ValueTask AddCommentAsync(RepositoryModel repo, ulong issueId, string content, ulong? replyTo)
+ {
+ var api = Api.C;
+ try
+ {
+ if (api.RequireRefreshToken() && !(await api.TryRefreshTokenAsync()))
+ {
+ api.ClearGateway();
+ return false;
+ }
+
+ await api.Gateway.Comment(repo.OwnerName, repo.Name, (long) issueId,
+ new AddCommentRequest { Content = content, ReplyTo = replyTo.HasValue ? (long) replyTo.Value : null });
+ return true;
+ }
+ catch (Exception e)
+ {
+ UIHelper.NotifyError(e);
+ Console.WriteLine(e);
+ return false;
+ }
+ }
+
+ public async ValueTask UpdateIssueAsync(RepositoryModel repo, long issueId, string? title, string? description, IEnumerable? tags)
+ {
+ var api = Api.C;
+ try
+ {
+ if (api.RequireRefreshToken() && !(await api.TryRefreshTokenAsync()))
+ {
+ api.ClearGateway();
+ return false;
+ }
+
+ StringBuilder? tagString = null;
+ if (tags != null)
+ {
+ new StringBuilder().AppendJoin(',', tags);
+ }
+
+ await api.Gateway.Edit(repo.OwnerName, repo.Name, issueId,
+ new UpdateIssueRequest { Title = title, Description = description, Tag = tagString?.ToString()});
+ return true;
+ }
+ catch (Exception e)
+ {
+ UIHelper.NotifyError(e);
+ Console.WriteLine(e);
+ return false;
+ }
+ }
public async ValueTask UpdateMembersFromServerAsync(RepositoryModel repo)
{
diff --git a/Flawless.Communication/Request/AddCommentRequest.cs b/Flawless.Communication/Request/AddCommentRequest.cs
new file mode 100644
index 0000000..2389bfb
--- /dev/null
+++ b/Flawless.Communication/Request/AddCommentRequest.cs
@@ -0,0 +1,3 @@
+namespace Flawless.Communication.Request;
+
+public record AddCommentRequest(string Content, ulong? ReplyTo);
\ No newline at end of file
diff --git a/Flawless.Communication/Request/CreateIssueRequest.cs b/Flawless.Communication/Request/CreateIssueRequest.cs
new file mode 100644
index 0000000..00c203e
--- /dev/null
+++ b/Flawless.Communication/Request/CreateIssueRequest.cs
@@ -0,0 +1,3 @@
+namespace Flawless.Communication.Request;
+
+public record CreateIssueRequest(string Title, string Description, string? Tag);
\ No newline at end of file
diff --git a/Flawless.Communication/Request/UpdateIssueRequest.cs b/Flawless.Communication/Request/UpdateIssueRequest.cs
new file mode 100644
index 0000000..dd57d1f
--- /dev/null
+++ b/Flawless.Communication/Request/UpdateIssueRequest.cs
@@ -0,0 +1,8 @@
+namespace Flawless.Communication.Request;
+
+public record UpdateIssueRequest
+(
+ string? Title,
+ string? Description,
+ string? Tag
+);
\ No newline at end of file
diff --git a/Flawless.Communication/Response/CommentResponse.cs b/Flawless.Communication/Response/CommentResponse.cs
new file mode 100644
index 0000000..77d0473
--- /dev/null
+++ b/Flawless.Communication/Response/CommentResponse.cs
@@ -0,0 +1,9 @@
+namespace Flawless.Communication.Response;
+
+public record CommentResponse(
+ ulong CommentId,
+ string Author,
+ string Content,
+ DateTime CreatedAt,
+ ulong? ReplyToId = null
+);
\ No newline at end of file
diff --git a/Flawless.Communication/Shared/IssueInfo.cs b/Flawless.Communication/Shared/IssueInfo.cs
new file mode 100644
index 0000000..e3d5828
--- /dev/null
+++ b/Flawless.Communication/Shared/IssueInfo.cs
@@ -0,0 +1,10 @@
+namespace Flawless.Communication.Shared;
+
+public record IssueInfo(
+ ulong Id, string Author, string Title, DateTime CreateAt, DateTime LastUpdate, bool closed, string? Tag);
+
+public record IssueDetailInfo(
+ ulong Id, string Author, string Title, string Description, DateTime CreateAt, DateTime LastUpdate, bool closed, string? Tag);
+
+public record IssueCommentInfo(
+ ulong Id, string Author, string Content, DateTime CreateAt, DateTime LastUpdate, ulong? ReplyTo);
\ No newline at end of file
diff --git a/Flawless.Server/Controllers/IssueController.cs b/Flawless.Server/Controllers/IssueController.cs
new file mode 100644
index 0000000..e2829e0
--- /dev/null
+++ b/Flawless.Server/Controllers/IssueController.cs
@@ -0,0 +1,240 @@
+using Flawless.Communication.Request;
+using Flawless.Communication.Response;
+using Flawless.Communication.Shared;
+using Flawless.Server.Models;
+using Flawless.Server.Services;
+using Microsoft.AspNetCore.Identity;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.EntityFrameworkCore;
+
+namespace Flawless.Server.Controllers;
+[ApiController, Microsoft.AspNetCore.Authorization.Authorize, Route("api/issue/{userName}/{repositoryName}")]
+public class IssueController(
+ UserManager userManager,
+ AppDbContext dbContext) : ControllerBase
+{
+ [HttpPost("create")]
+ public async Task> CreateIssueAsync(
+ string userName,
+ string repositoryName,
+ [FromBody] CreateIssueRequest request)
+ {
+ var user = (await userManager.GetUserAsync(HttpContext.User))!;
+ var grantIssue = await ValidateRepositoryAsync(userName, repositoryName, user, RepositoryRole.Reporter);
+ if (grantIssue is not Repository repository) return (ActionResult)grantIssue;
+
+ var issue = new RepositoryIssue
+ {
+ Repository = repository,
+ Author = user,
+ Title = request.Title,
+ CreatedAt = DateTime.UtcNow,
+ Descripion = request.Description,
+ Closed = false,
+ Tag = request.Tag
+ };
+
+ dbContext.RepositoryIssues.Add(issue);
+ await dbContext.SaveChangesAsync();
+
+ return Ok(new IssueInfo(
+ issue.Id,
+ issue.Author.UserName!,
+ issue.Title,
+ issue.CreatedAt,
+ issue.CreatedAt,
+ issue.Closed,
+ issue.Tag));
+ }
+
+ [HttpPost("{issueId}/comment")]
+ public async Task AddCommentAsync(
+ string userName,
+ string repositoryName,
+ ulong issueId,
+ [FromBody] AddCommentRequest request)
+ {
+ var user = (await userManager.GetUserAsync(HttpContext.User))!;
+ var grantIssue = await ValidateRepositoryAsync(userName, repositoryName, user, RepositoryRole.Reporter);
+ if (grantIssue is not Repository _) return (ActionResult) grantIssue;
+
+ var issue = await dbContext.RepositoryIssues
+ .Include(i => i.Repository)
+ .Include(repositoryIssue => repositoryIssue.Contents)
+ .FirstOrDefaultAsync(i => i.Id == issueId);
+
+ if (issue == null) return NotFound(new FailedResponse("Issue not found"));
+ if (issue.Closed) return BadRequest(new FailedResponse("Issue is closed"));
+
+ var comment = new RepositoryIssueContent
+ {
+ Issue = issue,
+ Author = user,
+ Content = request.Content,
+ CreatedAt = DateTime.UtcNow,
+ ReplyTo = request.ReplyTo.HasValue
+ ? issue.Contents.Find(v => v.Id == request.ReplyTo.Value) : null
+ };
+
+ issue.Contents.Add(comment);
+ await dbContext.SaveChangesAsync();
+
+ return Ok();
+ }
+
+ [HttpPost("{issueId}/close")]
+ public async Task CloseIssueAsync(
+ string userName,
+ string repositoryName,
+ ulong issueId)
+ {
+ var user = (await userManager.GetUserAsync(HttpContext.User))!;
+ var grantIssue = await ValidateRepositoryAsync(userName, repositoryName, user, RepositoryRole.Developer);
+ if (grantIssue is not Repository _) return (ActionResult) grantIssue;
+
+ var issue = await dbContext.RepositoryIssues.FindAsync(issueId);
+ if (issue == null) return NotFound(new FailedResponse("Issue not found"));
+
+ issue.Closed = true;
+ await dbContext.SaveChangesAsync();
+
+ return Ok();
+ }
+
+ [HttpGet("list")]
+ public async Task>> GetIssuesAsync(
+ string userName,
+ string repositoryName)
+ {
+ var user = (await userManager.GetUserAsync(HttpContext.User))!;
+ var grantIssue = await ValidateRepositoryAsync(userName, repositoryName, user, RepositoryRole.Reporter);
+ if (grantIssue is not Repository repository) return (ActionResult)grantIssue;
+
+ var issues = await dbContext.RepositoryIssues
+ .Where(i => i.Repository == repository)
+ .Include(i => i.Author)
+ .ToArrayAsync();
+
+ return Ok(new ListingResponse(issues));
+ }
+
+ [HttpGet("{issueId}")]
+ public async Task> GetIssueDetailsAsync(
+ string userName,
+ string repositoryName,
+ ulong issueId)
+ {
+ var user = (await userManager.GetUserAsync(HttpContext.User))!;
+ var grantIssue = await ValidateRepositoryAsync(userName, repositoryName, user, RepositoryRole.Reporter);
+ if (grantIssue is not Repository repo) return (ActionResult) grantIssue;
+
+ var issue = await dbContext.RepositoryIssues
+ .Include(i => i.Author)
+ .Include(i => i.Repository)
+ .Include(i => i.Contents)
+ .ThenInclude(c => c.Author)
+ .FirstOrDefaultAsync(x => x.Id == issueId && x.Repository == repo);
+
+ return issue == null
+ ? NotFound(new FailedResponse("Issue not found"))
+ : Ok(new IssueDetailInfo(
+ issue.Id,
+ issue.Author.UserName!,
+ issue.Title,
+ issue.Descripion ?? String.Empty,
+ issue.CreatedAt,
+ issue.Contents.MaxBy(x => x.CreatedAt)?.CreatedAt ?? issue.CreatedAt,
+ issue.Closed,
+ issue.Tag));
+ }
+
+ [HttpPatch("{issueId}/edit")]
+ public async Task UpdateIssueAsync(
+ string userName,
+ string repositoryName,
+ ulong issueId,
+ [FromBody] UpdateIssueRequest request)
+ {
+ var user = (await userManager.GetUserAsync(HttpContext.User))!;
+ var grantIssue = await ValidateRepositoryAsync(userName, repositoryName, user, RepositoryRole.Reporter);
+ if (grantIssue is not Repository repo) return (ActionResult) grantIssue;
+
+ var issue = await dbContext.RepositoryIssues
+ .Include(x => x.Author)
+ .FirstOrDefaultAsync(x => x.Id == issueId && x.Repository == repo);
+
+ if (issue == null) return NotFound(new FailedResponse("Issue not found"));
+ if (issue.Author != user && repo.Owner != user)
+ return BadRequest(new FailedResponse("You are not allowed to do this operation"));
+
+ if (!string.IsNullOrEmpty(request.Title)) issue.Title = request.Title;
+ if (!string.IsNullOrEmpty(request.Description)) issue.Descripion = request.Description;
+ if (request.Tag != null) issue.Tag = request.Tag;
+
+ await dbContext.SaveChangesAsync();
+ return Ok();
+ }
+
+ [HttpPost("{issueId}/reopen")]
+ public async Task ReopenIssueAsync(
+ string userName,
+ string repositoryName,
+ ulong issueId)
+ {
+ var user = (await userManager.GetUserAsync(HttpContext.User))!;
+ var grantIssue = await ValidateRepositoryAsync(userName, repositoryName, user, RepositoryRole.Developer);
+ if (grantIssue is not Repository _) return (ActionResult) grantIssue;
+
+ var issue = await dbContext.RepositoryIssues.FindAsync(issueId);
+ if (issue == null) return NotFound(new FailedResponse("Issue not found"));
+
+ issue.Closed = false;
+ await dbContext.SaveChangesAsync();
+
+ return Ok();
+ }
+
+ [HttpGet("{issueId}/comments")]
+ public async Task>> GetIssueCommentsAsync(
+ string userName,
+ string repositoryName,
+ ulong issueId)
+ {
+ var user = (await userManager.GetUserAsync(HttpContext.User))!;
+ var grantIssue = await ValidateRepositoryAsync(userName, repositoryName, user, RepositoryRole.Reporter);
+ if (grantIssue is not Repository repo) return (ActionResult) grantIssue;
+
+ var comments = await dbContext.RepositoryIssues
+ .Where(x => x.Id == issueId && x.Repository == repo)
+ .SelectMany(x => x.Contents)
+ .Include(c => c.Author)
+ .OrderBy(c => c.CreatedAt)
+ .Select(c => new CommentResponse(
+ c.Id,
+ c.Author.UserName!,
+ c.Content,
+ c.CreatedAt,
+ c.ReplyTo != null ? c.ReplyTo.Id : null))
+ .ToArrayAsync();
+
+ return Ok(new ListingResponse(comments));
+ }
+
+
+ private async ValueTask