1
0

feat: Add repository issue feature api.

This commit is contained in:
Ca2didi 2025-04-13 23:43:07 +08:00
parent 62ced75815
commit 62d7398035
13 changed files with 917 additions and 64 deletions

View File

@ -19,6 +19,7 @@
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AIdentityUser_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fc405819100144b0483c14b61d32c5aa215930_003F90_003F4d8e1a86_003FIdentityUser_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AIdentityUser_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F7d382df578ec93391918cfaa4ce7f4b8f35c9aed1241d6556dc9be26df13c_003FIdentityUser_002Ecs_002Fz_003A2_002D1/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AIdentityUser_00601_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fd991417b721d4ddab50a2b715d0ad696b138_003Fa2_003Fdb3874bc_003FIdentityUser_00601_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AIdentityUser_00601_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fd99404009f8a4a8da97cdbe0d6ecd5dd10800_003F1f_003Fe7cb9bcb_003FIdentityUser_00601_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AIReactiveObjectExtensions_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F404d064a80dc4960b93f90c9bd69770750810_003F5a_003F1516290d_003FIReactiveObjectExtensions_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AJwtBearerHandler_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F61447741f88e235f7cd1a276ef5abe648b2dee4b210873893d178b861c9d0_003FJwtBearerHandler_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003APath_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fa6b7f037ba7b44df80b8d3aa7e58eeb2e8e938_003F28_003F6a41ec86_003FPath_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>

View File

@ -35,6 +35,10 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Refit" Version="8.0.0" />
<PackageReference Include="Refitter.SourceGenerator" Version="1.5.3">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Semi.Avalonia" Version="11.2.1.6" />
<PackageReference Include="Semi.Avalonia.TreeDataGrid" Version="11.0.10.2" />
<PackageReference Include="ValueTaskSupplement" Version="1.1.0" />

View File

@ -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<Commit> Commits { get; } = new();
// todo cache depots?
public ObservableCollection<Depot> Depots { get; } = new();
public ObservableCollection<Lock> Locks { get; } = new();
public ObservableCollection<Issue> 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<string> Tags { get; } = new();
public ObservableCollection<Comment> 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;

View File

@ -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
{
/// <returns>A <see cref="Task"/> that completes when the request is finished.</returns>
/// <exception cref="ApiException">Thrown when the request returns a non-success status code.</exception>
[Post("/api/admin/user/delete/{username}")]
Task Delete(string username);
Task Delete(string username, CancellationToken cancellationToken = default);
/// <returns>A <see cref="Task"/> that completes when the request is finished.</returns>
/// <exception cref="ApiException">Thrown when the request returns a non-success status code.</exception>
[Post("/api/admin/user/enable/{username}")]
Task Enable(string username);
Task Enable(string username, CancellationToken cancellationToken = default);
/// <returns>A <see cref="Task"/> that completes when the request is finished.</returns>
/// <exception cref="ApiException">Thrown when the request returns a non-success status code.</exception>
[Post("/api/admin/user/disable/{username}")]
Task Disable(string username);
Task Disable(string username, CancellationToken cancellationToken = default);
/// <returns>A <see cref="Task"/> that completes when the request is finished.</returns>
/// <exception cref="ApiException">Thrown when the request returns a non-success status code.</exception>
[Headers("Content-Type: application/json")]
[Post("/api/admin/user/reset_password")]
Task ResetPassword([Body] ResetPasswordRequest body);
Task ResetPassword([Body] ResetPasswordRequest body, CancellationToken cancellationToken = default);
/// <returns>OK</returns>
/// <exception cref="ApiException">Thrown when the request returns a non-success status code.</exception>
[Headers("Accept: text/plain, application/json, text/json")]
[Get("/api/auth/status")]
Task<ServerStatusResponse> Status();
Task<ServerStatusResponse> Status(CancellationToken cancellationToken = default);
/// <returns>A <see cref="Task"/> that completes when the request is finished.</returns>
/// <exception cref="ApiException">Thrown when the request returns a non-success status code.</exception>
[Headers("Content-Type: application/json")]
[Post("/api/auth/register")]
Task Register([Body] RegisterRequest body);
Task Register([Body] RegisterRequest body, CancellationToken cancellationToken = default);
/// <returns>OK</returns>
/// <exception cref="ApiException">Thrown when the request returns a non-success status code.</exception>
[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<TokenInfo> Login([Body] LoginRequest body);
Task<TokenInfo> Login([Body] LoginRequest body, CancellationToken cancellationToken = default);
/// <returns>OK</returns>
/// <exception cref="ApiException">Thrown when the request returns a non-success status code.</exception>
[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<TokenInfo> Refresh([Body] TokenInfo body);
Task<TokenInfo> Refresh([Body] TokenInfo body, CancellationToken cancellationToken = default);
/// <returns>A <see cref="Task"/> that completes when the request is finished.</returns>
/// <exception cref="ApiException">Thrown when the request returns a non-success status code.</exception>
[Post("/api/auth/logout_all")]
Task LogoutAll();
Task LogoutAll(CancellationToken cancellationToken = default);
/// <returns>A <see cref="Task"/> that completes when the request is finished.</returns>
/// <exception cref="ApiException">Thrown when the request returns a non-success status code.</exception>
[Headers("Content-Type: application/json")]
[Post("/api/auth/renew_password")]
Task RenewPassword([Body] ResetPasswordRequest body);
Task RenewPassword([Body] ResetPasswordRequest body, CancellationToken cancellationToken = default);
/// <returns>A <see cref="Task"/> that completes when the request is finished.</returns>
/// <exception cref="ApiException">Thrown when the request returns a non-success status code.</exception>
[Get("/")]
Task Index();
Task Index(CancellationToken cancellationToken = default);
/// <returns>OK</returns>
/// <exception cref="ApiException">Thrown when the request returns a non-success status code.</exception>
[Headers("Accept: text/plain, application/json, text/json", "Content-Type: application/json")]
[Post("/api/issue/{userName}/{repositoryName}/create")]
Task<IssueInfo> Create(string userName, string repositoryName, [Body] CreateIssueRequest body, CancellationToken cancellationToken = default);
/// <returns>A <see cref="Task"/> that completes when the request is finished.</returns>
/// <exception cref="ApiException">Thrown when the request returns a non-success status code.</exception>
[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);
/// <returns>A <see cref="Task"/> that completes when the request is finished.</returns>
/// <exception cref="ApiException">Thrown when the request returns a non-success status code.</exception>
[Post("/api/issue/{userName}/{repositoryName}/{issueId}/close")]
Task Close(string userName, string repositoryName, long issueId, CancellationToken cancellationToken = default);
/// <returns>OK</returns>
/// <exception cref="ApiException">Thrown when the request returns a non-success status code.</exception>
[Headers("Accept: text/plain, application/json, text/json")]
[Get("/api/issue/{userName}/{repositoryName}/list")]
Task<IssueInfoListingResponse> List(string userName, string repositoryName, CancellationToken cancellationToken = default);
/// <returns>OK</returns>
/// <exception cref="ApiException">Thrown when the request returns a non-success status code.</exception>
[Headers("Accept: text/plain, application/json, text/json")]
[Get("/api/issue/{userName}/{repositoryName}/{issueId}")]
Task<IssueDetailInfo> Issue(string userName, string repositoryName, long issueId, CancellationToken cancellationToken = default);
/// <returns>A <see cref="Task"/> that completes when the request is finished.</returns>
/// <exception cref="ApiException">Thrown when the request returns a non-success status code.</exception>
[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);
/// <returns>A <see cref="Task"/> that completes when the request is finished.</returns>
/// <exception cref="ApiException">Thrown when the request returns a non-success status code.</exception>
[Post("/api/issue/{userName}/{repositoryName}/{issueId}/reopen")]
Task Reopen(string userName, string repositoryName, long issueId, CancellationToken cancellationToken = default);
/// <returns>OK</returns>
/// <exception cref="ApiException">Thrown when the request returns a non-success status code.</exception>
[Headers("Accept: text/plain, application/json, text/json")]
[Get("/api/issue/{userName}/{repositoryName}/{issueId}/comments")]
Task<CommentResponseListingResponse> Comments(string userName, string repositoryName, long issueId, CancellationToken cancellationToken = default);
/// <returns>A <see cref="Task"/> that completes when the request is finished.</returns>
/// <exception cref="ApiException">Thrown when the request returns a non-success status code.</exception>
[Post("/api/repo/{userName}/{repositoryName}/delete_repo")]
Task DeleteRepo(string repositoryName, string userName);
Task DeleteRepo(string userName, string repositoryName, CancellationToken cancellationToken = default);
/// <returns>OK</returns>
/// <exception cref="ApiException">Thrown when the request returns a non-success status code.</exception>
[Headers("Accept: text/plain, application/json, text/json")]
[Get("/api/repo/{userName}/{repositoryName}/get_info")]
Task<RepositoryInfoResponse> GetInfo(string repositoryName, string userName);
Task<RepositoryInfoResponse> GetInfo(string userName, string repositoryName, CancellationToken cancellationToken = default);
/// <returns>A <see cref="Task"/> that completes when the request is finished.</returns>
/// <exception cref="ApiException">Thrown when the request returns a non-success status code.</exception>
[Post("/api/repo/{userName}/{repositoryName}/archive_repo")]
Task ArchiveRepo(string repositoryName, string userName);
Task ArchiveRepo(string userName, string repositoryName, CancellationToken cancellationToken = default);
/// <returns>A <see cref="Task"/> that completes when the request is finished.</returns>
/// <exception cref="ApiException">Thrown when the request returns a non-success status code.</exception>
[Post("/api/repo/{userName}/{repositoryName}/unarchive_repo")]
Task UnarchiveRepo(string repositoryName, string userName);
Task UnarchiveRepo(string userName, string repositoryName, CancellationToken cancellationToken = default);
/// <returns>OK</returns>
/// <exception cref="ApiException">Thrown when the request returns a non-success status code.</exception>
[Headers("Accept: text/plain, application/json, text/json")]
[Get("/api/repo/{userName}/{repositoryName}/get_users")]
Task<RepoUserRoleListingResponse> GetUsers(string repositoryName, string userName);
Task<RepoUserRoleListingResponse> GetUsers(string userName, string repositoryName, CancellationToken cancellationToken = default);
/// <returns>OK</returns>
/// <exception cref="ApiException">Thrown when the request returns a non-success status code.</exception>
[Headers("Accept: text/plain, application/json, text/json")]
[Post("/api/repo/{userName}/{repositoryName}/fetch_manifest")]
Task<CommitManifest> FetchManifest(string userName, string repositoryName, [Query] string commitId);
Task<CommitManifest> FetchManifest(string userName, string repositoryName, [Query] string commitId, CancellationToken cancellationToken = default);
/// <returns>OK</returns>
/// <exception cref="ApiException">Thrown when the request returns a non-success status code.</exception>
@ -118,48 +168,48 @@ namespace Flawless.Client.Remote
/// <exception cref="ApiException">Thrown when the request returns a non-success status code.</exception>
[Headers("Accept: text/plain, application/json, text/json")]
[Post("/api/repo/{userName}/{repositoryName}/list_commit")]
Task<RepositoryCommitResponseListingResponse> ListCommit(string userName, string repositoryName);
Task<RepositoryCommitResponseListingResponse> ListCommit(string userName, string repositoryName, CancellationToken cancellationToken = default);
/// <returns>OK</returns>
/// <exception cref="ApiException">Thrown when the request returns a non-success status code.</exception>
[Headers("Accept: text/plain, application/json, text/json")]
[Post("/api/repo/{userName}/{repositoryName}/list_locked_files")]
Task<LockFileInfoListingResponse> ListLockedFiles(string userName, string repositoryName);
Task<LockFileInfoListingResponse> ListLockedFiles(string userName, string repositoryName, CancellationToken cancellationToken = default);
/// <returns>OK</returns>
/// <exception cref="ApiException">Thrown when the request returns a non-success status code.</exception>
[Headers("Accept: text/plain, application/json, text/json")]
[Get("/api/repo/{userName}/{repositoryName}/peek_commit")]
Task<GuidPeekResponse> PeekCommit(string userName, string repositoryName);
Task<GuidPeekResponse> PeekCommit(string userName, string repositoryName, CancellationToken cancellationToken = default);
/// <returns>A <see cref="Task"/> that completes when the request is finished.</returns>
/// <exception cref="ApiException">Thrown when the request returns a non-success status code.</exception>
[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);
/// <returns>A <see cref="Task"/> that completes when the request is finished.</returns>
/// <exception cref="ApiException">Thrown when the request returns a non-success status code.</exception>
[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);
/// <returns>OK</returns>
/// <exception cref="ApiException">Thrown when the request returns a non-success status code.</exception>
[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<CommitSuccessResponse> CreateCommit(string userName, string repositoryName, StreamPart depot, string message, IEnumerable<string> workspaceSnapshot, IEnumerable<string> requiredDepots, string mainDepotId);
Task<CommitSuccessResponse> CreateCommit(string userName, string repositoryName, StreamPart depot, string message, IEnumerable<string> workspaceSnapshot, IEnumerable<string> requiredDepots, string mainDepotId, CancellationToken cancellationToken = default);
/// <returns>OK</returns>
/// <exception cref="ApiException">Thrown when the request returns a non-success status code.</exception>
[Headers("Accept: text/plain, application/json, text/json")]
[Get("/api/repo_list")]
Task<RepositoryInfoResponseListingResponse> RepoList();
Task<RepositoryInfoResponseListingResponse> RepoList(CancellationToken cancellationToken = default);
/// <returns>OK</returns>
/// <exception cref="ApiException">Thrown when the request returns a non-success status code.</exception>
[Headers("Accept: text/plain, application/json, text/json")]
[Post("/api/repo_create")]
Task<RepositoryInfoResponse> RepoCreate([Query] string repositoryName, [Query] string description);
Task<RepositoryInfoResponse> RepoCreate([Query] string repositoryName, [Query] string description, CancellationToken cancellationToken = default);
/// <returns>A <see cref="Task"/> that completes when the request is finished.</returns>
/// <exception cref="ApiException">Thrown when the request returns a non-success status code.</exception>
@ -169,39 +219,42 @@ namespace Flawless.Client.Remote
/// <returns>A <see cref="Task"/> that completes when the request is finished.</returns>
/// <exception cref="ApiException">Thrown when the request returns a non-success status code.</exception>
[Post("/api/delete_user")]
Task DeleteUser([Query] string repositoryName, [Query] string delUser);
Task DeleteUser([Query] string repositoryName, [Query] string delUser, CancellationToken cancellationToken = default);
/// <returns>A <see cref="Task"/> that completes when the request is finished.</returns>
/// <exception cref="ApiException">Thrown when the request returns a non-success status code.</exception>
[Headers("Content-Type: application/json")]
[Post("/api/user/update_info")]
Task UpdateInfo([Body] UserInfoModifyResponse body);
Task UpdateInfo([Body] UserInfoModifyResponse body, CancellationToken cancellationToken = default);
/// <returns>A <see cref="Task"/> that completes when the request is finished.</returns>
/// <exception cref="ApiException">Thrown when the request returns a non-success status code.</exception>
[Headers("Content-Type: application/json")]
[Post("/api/user/update_email")]
Task UpdateEmail([Body] UserContactModifyResponse body);
Task UpdateEmail([Body] UserContactModifyResponse body, CancellationToken cancellationToken = default);
/// <returns>A <see cref="Task"/> that completes when the request is finished.</returns>
/// <exception cref="ApiException">Thrown when the request returns a non-success status code.</exception>
[Headers("Content-Type: application/json")]
[Post("/api/user/update_phone")]
Task UpdatePhone([Body] UserContactModifyResponse body);
Task UpdatePhone([Body] UserContactModifyResponse body, CancellationToken cancellationToken = default);
/// <returns>OK</returns>
/// <exception cref="ApiException">Thrown when the request returns a non-success status code.</exception>
[Headers("Accept: text/plain, application/json, text/json")]
[Get("/api/user/get_info")]
Task<UserInfoResponse> GetInfo([Query] string username);
Task<UserInfoResponse> GetInfo([Query] string username, CancellationToken cancellationToken = default);
/// <returns>OK</returns>
/// <exception cref="ApiException">Thrown when the request returns a non-success status code.</exception>
[Headers("Accept: text/plain, application/json, text/json")]
[Get("/api/user/query_info")]
Task<UserInfoResponseListingResponse> QueryInfo([Query] string keyword);
Task<UserInfoResponseListingResponse> QueryInfo([Query] string keyword, CancellationToken cancellationToken = default);
/// <returns>A <see cref="Task"/> that completes when the request is finished.</returns>
/// <exception cref="ApiException">Thrown when the request returns a non-success status code.</exception>
[Get("/api/user/delete")]
Task Delete();
Task Delete(CancellationToken cancellationToken = default);
}
@ -210,7 +263,7 @@ namespace Flawless.Client.Remote
//----------------------
// <auto-generated>
// 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)
// </auto-generated>
//----------------------
@ -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<CommentResponse> 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<IssueInfo> 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

View File

@ -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<RepositoryService>
return false;
}
}
public async ValueTask<bool> 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<bool> 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<IssueInfo?> CreateIssueAsync(RepositoryModel repo, string title, string description, IEnumerable<string>? 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<bool> 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<bool> 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<bool> 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<bool> UpdateIssueAsync(RepositoryModel repo, long issueId, string? title, string? description, IEnumerable<string>? 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<bool> UpdateMembersFromServerAsync(RepositoryModel repo)
{

View File

@ -0,0 +1,3 @@
namespace Flawless.Communication.Request;
public record AddCommentRequest(string Content, ulong? ReplyTo);

View File

@ -0,0 +1,3 @@
namespace Flawless.Communication.Request;
public record CreateIssueRequest(string Title, string Description, string? Tag);

View File

@ -0,0 +1,8 @@
namespace Flawless.Communication.Request;
public record UpdateIssueRequest
(
string? Title,
string? Description,
string? Tag
);

View File

@ -0,0 +1,9 @@
namespace Flawless.Communication.Response;
public record CommentResponse(
ulong CommentId,
string Author,
string Content,
DateTime CreatedAt,
ulong? ReplyToId = null
);

View File

@ -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);

View File

@ -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<AppUser> userManager,
AppDbContext dbContext) : ControllerBase
{
[HttpPost("create")]
public async Task<ActionResult<IssueInfo>> 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<IActionResult> 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<IActionResult> 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<ActionResult<ListingResponse<IssueInfo>>> 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<RepositoryIssue>(issues));
}
[HttpGet("{issueId}")]
public async Task<ActionResult<IssueDetailInfo>> 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<IActionResult> 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<IActionResult> 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<ActionResult<ListingResponse<CommentResponse>>> 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<CommentResponse>(comments));
}
private async ValueTask<object> 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;
}
}

View File

@ -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<RepositoryIssueContent> 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; }
}

View File

@ -12,6 +12,8 @@ public class AppDbContext(DbContextOptions<AppDbContext> options)
public DbSet<AppUserRefreshKey> RefreshTokens { get; set; }
public DbSet<Repository> Repositories { get; set; }
public DbSet<RepositoryIssue> RepositoryIssues { get; set; }
public async ValueTask<(bool existed, bool authorized)> CheckRepositoryExistedAuthorizedAsync(
AppUser owner, string name, AppUser user, RepositoryRole role)