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 ValidateRepositoryAsync( + string userName, string repositoryName, AppUser user, RepositoryRole role) + { + // 复用RepositoryInnieController中的验证逻辑 + var rp = await dbContext.Repositories + .Include(r => r.Members) + .ThenInclude(m => m.User) + .Include(r => r.Owner) + .FirstOrDefaultAsync(r => r.Owner.UserName == userName && r.Name == repositoryName); + + if (rp == null) return NotFound(new FailedResponse($"Could not find repository {userName}:{repositoryName}")); + if (rp.Owner != user && !rp.Members.Any(m => m.User == user && m.Role >= role)) + return Unauthorized(new FailedResponse("You are not allowed to do this operation")); + + return rp; + } +} \ No newline at end of file diff --git a/Flawless.Server/Models/RepositoryIssue.cs b/Flawless.Server/Models/RepositoryIssue.cs new file mode 100644 index 0000000..6a62e88 --- /dev/null +++ b/Flawless.Server/Models/RepositoryIssue.cs @@ -0,0 +1,50 @@ +using System.ComponentModel.DataAnnotations; + +namespace Flawless.Server.Models; + +public class RepositoryIssue +{ + [Key, Required] + public ulong Id { get; set; } + + [Required] + public required Repository Repository { get; set; } + + [Required] + public required AppUser Author { get; set; } + + [Required] + public required string Title { get; set; } + + [Required] + public required DateTime CreatedAt { get; set; } + + public string? Descripion { get; set; } + + [Required] + public required bool Closed { get; set; } + + public string? Tag { get; set; } + + public List Contents { get; set; } = new(); +} + +public class RepositoryIssueContent +{ + [Key, Required] + public ulong Id { get; set; } + + [Required] + public RepositoryIssue Issue { get; set; } + + [Required] + public required AppUser Author { get; set; } + + [Required] + public required DateTime CreatedAt { get; set; } + + [Required] + public required string Content { get; set; } + + public RepositoryIssueContent? ReplyTo { get; set; } +} \ No newline at end of file diff --git a/Flawless.Server/Services/AppDbContext.cs b/Flawless.Server/Services/AppDbContext.cs index c940d01..aab781d 100644 --- a/Flawless.Server/Services/AppDbContext.cs +++ b/Flawless.Server/Services/AppDbContext.cs @@ -12,6 +12,8 @@ public class AppDbContext(DbContextOptions options) public DbSet RefreshTokens { get; set; } public DbSet Repositories { get; set; } + + public DbSet RepositoryIssues { get; set; } public async ValueTask<(bool existed, bool authorized)> CheckRepositoryExistedAuthorizedAsync( AppUser owner, string name, AppUser user, RepositoryRole role)