1
0

Compare commits

..

No commits in common. "16294b7103f7ed6309fbda7e3aa6c9e8ff6031c0" and "3afafd4e917ef57c39ff14db28ad2395b14d24f5" have entirely different histories.

26 changed files with 246 additions and 237 deletions

View File

@ -6,13 +6,6 @@ using ReactiveUI.SourceGenerators;
namespace Flawless.Client.Models; namespace Flawless.Client.Models;
public enum WebhookEventType
{
Push,
IssueCreated,
IssueUpdate
}
public partial class RepositoryModel : ReactiveModel public partial class RepositoryModel : ReactiveModel
{ {
public static string GetStandaloneName(string name, string ownerName) public static string GetStandaloneName(string name, string ownerName)
@ -65,7 +58,7 @@ public partial class RepositoryModel : ReactiveModel
public partial class Issue : ReactiveModel public partial class Issue : ReactiveModel
{ {
[Reactive] private int _id; [Reactive] private ulong _id;
[Reactive] private string _title; [Reactive] private string _title;
@ -86,7 +79,7 @@ public partial class RepositoryModel : ReactiveModel
public partial class Comment : ReactiveModel public partial class Comment : ReactiveModel
{ {
[Reactive] private int _id; [Reactive] private ulong _id;
[Reactive] private string _content; [Reactive] private string _content;
@ -94,7 +87,7 @@ public partial class RepositoryModel : ReactiveModel
[Reactive] private DateTime _createdAt; [Reactive] private DateTime _createdAt;
[Reactive] private int? _replyTo; [Reactive] private ulong? _replyTo;
} }
public partial class Member : ReactiveModel public partial class Member : ReactiveModel

View File

@ -8,7 +8,7 @@ public enum RepositoryResetMethod
Keep, Keep,
/// <summary> /// <summary>
/// Tracked files will being reset, changes will being merged. /// Tracked files will being reset, changes will being reset.
/// </summary> /// </summary>
Soft, Soft,
@ -16,4 +16,14 @@ public enum RepositoryResetMethod
/// All files will being reset, changes will being reset, changes list will being cleaned. /// All files will being reset, changes will being reset, changes list will being cleaned.
/// </summary> /// </summary>
Hard, Hard,
/// <summary>
/// Tracked files will being reset, changes will being merged.
/// </summary>
Merge,
/// <summary>
/// All files will being reset, changes will being merged.
/// </summary>
HardMerge
} }

View File

@ -90,7 +90,7 @@ namespace Flawless.Client.Remote
/// <exception cref="ApiException">Thrown when the request returns a non-success status code.</exception> /// <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")]
[Get("/api/admin/logs")] [Get("/api/admin/logs")]
Task<ICollection<LogEntryResponse>> Logs([Query] System.DateTimeOffset startTime, [Query] System.DateTimeOffset endTime, [Query] int level, [Query] int page, [Query] int pageSize, CancellationToken cancellationToken = default); Task<ICollection<LogEntryResponse>> Logs([Query] System.DateTimeOffset? startTime, [Query] System.DateTimeOffset? endTime, [Query] LogLevel? level, [Query] int? page, [Query] int? pageSize, CancellationToken cancellationToken = default);
/// <returns>OK</returns> /// <returns>OK</returns>
/// <exception cref="ApiException">Thrown when the request returns a non-success status code.</exception> /// <exception cref="ApiException">Thrown when the request returns a non-success status code.</exception>
@ -148,7 +148,7 @@ namespace Flawless.Client.Remote
/// <exception cref="ApiException">Thrown when the request returns a non-success status code.</exception> /// <exception cref="ApiException">Thrown when the request returns a non-success status code.</exception>
[Headers("Content-Type: application/json")] [Headers("Content-Type: application/json")]
[Post("/api/issue/{userName}/{repositoryName}/{issueId}/comment")] [Post("/api/issue/{userName}/{repositoryName}/{issueId}/comment")]
Task Comment(string userName, string repositoryName, int issueId, [Body] AddCommentRequest body, CancellationToken cancellationToken = default); 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> /// <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> /// <exception cref="ApiException">Thrown when the request returns a non-success status code.</exception>
@ -165,13 +165,13 @@ namespace Flawless.Client.Remote
/// <exception cref="ApiException">Thrown when the request returns a non-success status code.</exception> /// <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")]
[Get("/api/issue/{userName}/{repositoryName}/{issueId}")] [Get("/api/issue/{userName}/{repositoryName}/{issueId}")]
Task<IssueDetailInfo> Issue(string userName, string repositoryName, int issueId, CancellationToken cancellationToken = default); 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> /// <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> /// <exception cref="ApiException">Thrown when the request returns a non-success status code.</exception>
[Headers("Content-Type: application/json")] [Headers("Content-Type: application/json")]
[Patch("/api/issue/{userName}/{repositoryName}/{issueId}/edit")] [Patch("/api/issue/{userName}/{repositoryName}/{issueId}/edit")]
Task Edit(string userName, string repositoryName, int issueId, [Body] UpdateIssueRequest body, CancellationToken cancellationToken = default); 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> /// <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> /// <exception cref="ApiException">Thrown when the request returns a non-success status code.</exception>
@ -182,7 +182,7 @@ namespace Flawless.Client.Remote
/// <exception cref="ApiException">Thrown when the request returns a non-success status code.</exception> /// <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")]
[Get("/api/issue/{userName}/{repositoryName}/{issueId}/comments")] [Get("/api/issue/{userName}/{repositoryName}/{issueId}/comments")]
Task<CommentResponseListingResponse> Comments(string userName, string repositoryName, int issueId, CancellationToken cancellationToken = default); 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> /// <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> /// <exception cref="ApiException">Thrown when the request returns a non-success status code.</exception>
@ -282,7 +282,7 @@ namespace Flawless.Client.Remote
/// <returns>OK</returns> /// <returns>OK</returns>
/// <exception cref="ApiException">Thrown when the request returns a non-success status code.</exception> /// <exception cref="ApiException">Thrown when the request returns a non-success status code.</exception>
[Multipart] [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")] [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, CancellationToken cancellationToken = default); Task<CommitSuccessResponse> CreateCommit(string userName, string repositoryName, StreamPart depot, string message, IEnumerable<string> workspaceSnapshot, IEnumerable<string> requiredDepots, string mainDepotId, CancellationToken cancellationToken = default);
@ -379,9 +379,13 @@ namespace Flawless.Client.Remote
[System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.4.0.0 (NJsonSchema v11.3.2.0 (Newtonsoft.Json v13.0.0.0))")] [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.4.0.0 (NJsonSchema v11.3.2.0 (Newtonsoft.Json v13.0.0.0))")]
public partial class AddCommentRequest public partial class AddCommentRequest
{ {
public string Content;
public int? ReplyTo; [JsonPropertyName("content")]
public string Content { get; set; }
[JsonPropertyName("replyTo")]
public long? ReplyTo { get; set; }
} }
[System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.4.0.0 (NJsonSchema v11.3.2.0 (Newtonsoft.Json v13.0.0.0))")] [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.4.0.0 (NJsonSchema v11.3.2.0 (Newtonsoft.Json v13.0.0.0))")]
@ -389,7 +393,7 @@ namespace Flawless.Client.Remote
{ {
[JsonPropertyName("commentId")] [JsonPropertyName("commentId")]
public int CommentId { get; set; } public long CommentId { get; set; }
[JsonPropertyName("author")] [JsonPropertyName("author")]
public string Author { get; set; } public string Author { get; set; }
@ -401,7 +405,7 @@ namespace Flawless.Client.Remote
public System.DateTimeOffset CreatedAt { get; set; } public System.DateTimeOffset CreatedAt { get; set; }
[JsonPropertyName("replyToId")] [JsonPropertyName("replyToId")]
public int? ReplyToId { get; set; } public long? ReplyToId { get; set; }
} }
@ -536,7 +540,7 @@ namespace Flawless.Client.Remote
{ {
[JsonPropertyName("id")] [JsonPropertyName("id")]
public int Id { get; set; } public long Id { get; set; }
[JsonPropertyName("author")] [JsonPropertyName("author")]
public string Author { get; set; } public string Author { get; set; }
@ -566,7 +570,7 @@ namespace Flawless.Client.Remote
{ {
[JsonPropertyName("id")] [JsonPropertyName("id")]
public int Id { get; set; } public long Id { get; set; }
[JsonPropertyName("author")] [JsonPropertyName("author")]
public string Author { get; set; } public string Author { get; set; }
@ -636,6 +640,26 @@ namespace Flawless.Client.Remote
} }
[System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.4.0.0 (NJsonSchema v11.3.2.0 (Newtonsoft.Json v13.0.0.0))")]
public enum LogLevel
{
_0 = 0,
_1 = 1,
_2 = 2,
_3 = 3,
_4 = 4,
_5 = 5,
_6 = 6,
}
[System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.4.0.0 (NJsonSchema v11.3.2.0 (Newtonsoft.Json v13.0.0.0))")] [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.4.0.0 (NJsonSchema v11.3.2.0 (Newtonsoft.Json v13.0.0.0))")]
public partial class LoginRequest public partial class LoginRequest
{ {
@ -863,7 +887,7 @@ namespace Flawless.Client.Remote
public string NickName { get; set; } public string NickName { get; set; }
[JsonPropertyName("gender")] [JsonPropertyName("gender")]
public int Gender { get; set; } public UserSex? Gender { get; set; }
[JsonPropertyName("bio")] [JsonPropertyName("bio")]
public string Bio { get; set; } public string Bio { get; set; }
@ -925,13 +949,13 @@ namespace Flawless.Client.Remote
public enum UserSex public enum UserSex
{ {
_0 = 0, Unset = 0,
_1 = 1, Male = 1,
_2 = 2, Female = 2,
_3 = 3, WalmartPlasticBag = 3,
} }
@ -943,13 +967,25 @@ namespace Flawless.Client.Remote
public string TargetUrl { get; set; } public string TargetUrl { get; set; }
[JsonPropertyName("eventType")] [JsonPropertyName("eventType")]
public int EventType { get; set; } public WebhookEventType EventType { get; set; }
[JsonPropertyName("secret")] [JsonPropertyName("secret")]
public string Secret { get; set; } public string Secret { get; set; }
} }
[System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.4.0.0 (NJsonSchema v11.3.2.0 (Newtonsoft.Json v13.0.0.0))")]
public enum WebhookEventType
{
Push = 0,
IssueCreated = 1,
IssueUpdate = 2
}
[System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.4.0.0 (NJsonSchema v11.3.2.0 (Newtonsoft.Json v13.0.0.0))")] [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.4.0.0 (NJsonSchema v11.3.2.0 (Newtonsoft.Json v13.0.0.0))")]
public partial class WebhookResponse public partial class WebhookResponse
{ {
@ -961,7 +997,7 @@ namespace Flawless.Client.Remote
public string TargetUrl { get; set; } public string TargetUrl { get; set; }
[JsonPropertyName("eventType")] [JsonPropertyName("eventType")]
public int EventType { get; set; } public WebhookEventType EventType { get; set; }
[JsonPropertyName("isActive")] [JsonPropertyName("isActive")]
public bool IsActive { get; set; } public bool IsActive { get; set; }

View File

@ -298,7 +298,7 @@ public class RepositoryService : BaseService<RepositoryService>
} }
var issues = (await api.Gateway.List(repo.OwnerName, repo.Name)) var issues = (await api.Gateway.List(repo.OwnerName, repo.Name))
.Result.ToImmutableDictionary(x => x.Id); .Result.ToImmutableDictionary(x => (ulong) x.Id);
for (var i = 0; i < repo.Issues.Count; i++) for (var i = 0; i < repo.Issues.Count; i++)
{ {
@ -350,7 +350,7 @@ public class RepositoryService : BaseService<RepositoryService>
return true; return true;
} }
public async ValueTask<bool> UpdateIssueDetailsFromServerAsync(RepositoryModel repo, int issueId, bool titleOnly) public async ValueTask<bool> UpdateIssueDetailsFromServerAsync(RepositoryModel repo, ulong issueId, bool titleOnly)
{ {
var api = Api.C; var api = Api.C;
try try
@ -361,7 +361,7 @@ public class RepositoryService : BaseService<RepositoryService>
return false; return false;
} }
var issue = await api.Gateway.Issue(repo.Name, repo.OwnerName, issueId); var issue = await api.Gateway.Issue(repo.Name, repo.OwnerName, (long)issueId);
var entity = repo.Issues.FirstOrDefault(x => x.Id == issueId); var entity = repo.Issues.FirstOrDefault(x => x.Id == issueId);
var tags = issue.Tag.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); var tags = issue.Tag.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
@ -371,7 +371,7 @@ public class RepositoryService : BaseService<RepositoryService>
entity = new RepositoryModel.Issue entity = new RepositoryModel.Issue
{ {
Author = issue.Author, Author = issue.Author,
Id = issue.Id, Id = (ulong) issue.Id,
Title = issue.Title, Title = issue.Title,
Description = null, Description = null,
Closed = issue.Closed, Closed = issue.Closed,
@ -384,16 +384,16 @@ public class RepositoryService : BaseService<RepositoryService>
if (!titleOnly) if (!titleOnly)
{ {
var details = (await api.Gateway.Comments(repo.Name, repo.OwnerName, issueId)) var details = (await api.Gateway.Comments(repo.Name, repo.OwnerName, (long)issueId))
.Result.ToImmutableDictionary(x => x.CommentId); .Result.ToImmutableDictionary(x => x.CommentId);
entity.Comments.AddRange(details.Select(x => new RepositoryModel.Comment entity.Comments.AddRange(details.Select(x => new RepositoryModel.Comment
{ {
Id = x.Key, Id = (ulong)x.Key,
Author = x.Value.Author, Author = x.Value.Author,
Content = x.Value.Content, Content = x.Value.Content,
CreatedAt = x.Value.CreatedAt.UtcDateTime, CreatedAt = x.Value.CreatedAt.UtcDateTime,
ReplyTo = x.Value.ReplyToId.HasValue ? x.Value.ReplyToId.Value : null ReplyTo = x.Value.ReplyToId.HasValue ? (ulong)x.Value.ReplyToId.Value : null
})); }));
} }
} }
@ -409,27 +409,27 @@ public class RepositoryService : BaseService<RepositoryService>
if (!titleOnly) if (!titleOnly)
{ {
var details = (await api.Gateway.Comments(repo.Name, repo.OwnerName, issueId)) var details = (await api.Gateway.Comments(repo.Name, repo.OwnerName, (long)issueId))
.Result.ToImmutableDictionary(x => x.CommentId); .Result.ToImmutableDictionary(x => x.CommentId);
for (var i = 0; i < entity.Comments.Count; i++) for (var i = 0; i < entity.Comments.Count; i++)
{ {
var c = entity.Comments[i]; var c = entity.Comments[i];
if (!details.ContainsKey(c.Id)) repo.Issues.RemoveAt(i); if (!details.ContainsKey((long) c.Id)) repo.Issues.RemoveAt(i);
} }
foreach (var (key, value) in details) foreach (var (key, value) in details)
{ {
var d = entity.Comments.FirstOrDefault(x => x.Id == key); var d = entity.Comments.FirstOrDefault(x => x.Id == (ulong) key);
if (d == null) if (d == null)
{ {
var c = new RepositoryModel.Comment var c = new RepositoryModel.Comment
{ {
Id = key, Id = (ulong)key,
Author = value.Author, Author = value.Author,
Content = value.Content, Content = value.Content,
CreatedAt = value.CreatedAt.UtcDateTime, CreatedAt = value.CreatedAt.UtcDateTime,
ReplyTo = value.ReplyToId.HasValue ? value.ReplyToId.Value : null ReplyTo = value.ReplyToId.HasValue ? (ulong)value.ReplyToId.Value : null
}; };
entity.Comments.Add(c); entity.Comments.Add(c);
@ -439,7 +439,7 @@ public class RepositoryService : BaseService<RepositoryService>
d.Author = value.Author; d.Author = value.Author;
d.Content = value.Content; d.Content = value.Content;
d.CreatedAt = value.CreatedAt.UtcDateTime; d.CreatedAt = value.CreatedAt.UtcDateTime;
d.ReplyTo = value.ReplyToId.HasValue ? value.ReplyToId.Value : null; d.ReplyTo = value.ReplyToId.HasValue ? (ulong)value.ReplyToId.Value : null;
} }
} }
} }
@ -457,7 +457,7 @@ public class RepositoryService : BaseService<RepositoryService>
return true; return true;
} }
public async ValueTask<int?> CreateIssueAsync(RepositoryModel repo, string title, string description, IEnumerable<string>? tags) public async ValueTask<ulong?> CreateIssueAsync(RepositoryModel repo, string title, string description, IEnumerable<string>? tags)
{ {
var api = Api.C; var api = Api.C;
try try
@ -482,7 +482,7 @@ public class RepositoryService : BaseService<RepositoryService>
var entity = new RepositoryModel.Issue var entity = new RepositoryModel.Issue
{ {
Author = issue.Author, Author = issue.Author,
Id = issue.Id, Id = (ulong) issue.Id,
Title = issue.Title, Title = issue.Title,
Description = null, Description = null,
Closed = issue.Closed, Closed = issue.Closed,
@ -504,7 +504,7 @@ public class RepositoryService : BaseService<RepositoryService>
} }
} }
public async ValueTask<bool> CloseIssueAsync(RepositoryModel repo, int issueId) public async ValueTask<bool> CloseIssueAsync(RepositoryModel repo, ulong issueId)
{ {
var api = Api.C; var api = Api.C;
try try
@ -527,7 +527,7 @@ public class RepositoryService : BaseService<RepositoryService>
} }
} }
public async ValueTask<bool> ReopenIssueAsync(RepositoryModel repo, int issueId) public async ValueTask<bool> ReopenIssueAsync(RepositoryModel repo, ulong issueId)
{ {
var api = Api.C; var api = Api.C;
try try
@ -550,7 +550,7 @@ public class RepositoryService : BaseService<RepositoryService>
} }
} }
public async ValueTask<bool> AddCommentAsync(RepositoryModel repo, int issueId, string content, int? replyTo) public async ValueTask<bool> AddCommentAsync(RepositoryModel repo, ulong issueId, string content, ulong? replyTo)
{ {
var api = Api.C; var api = Api.C;
try try
@ -561,12 +561,8 @@ public class RepositoryService : BaseService<RepositoryService>
return false; return false;
} }
var req = new AddCommentRequest await api.Gateway.Comment(repo.OwnerName, repo.Name, (long) issueId,
{ new AddCommentRequest { Content = content, ReplyTo = replyTo.HasValue ? (long) replyTo.Value : null });
Content = content, ReplyTo = replyTo.HasValue ? replyTo.Value : null
};
await api.Gateway.Comment(repo.OwnerName, repo.Name, issueId, req);
return true; return true;
} }
catch (Exception e) catch (Exception e)
@ -579,7 +575,7 @@ public class RepositoryService : BaseService<RepositoryService>
public async ValueTask<bool> UpdateIssueAsync(RepositoryModel repo, int issueId, string? title, string? description, IEnumerable<string>? tags) public async ValueTask<bool> UpdateIssueAsync(RepositoryModel repo, ulong issueId, string? title, string? description, IEnumerable<string>? tags)
{ {
var api = Api.C; var api = Api.C;
try try
@ -596,7 +592,7 @@ public class RepositoryService : BaseService<RepositoryService>
new StringBuilder().AppendJoin(',', tags); new StringBuilder().AppendJoin(',', tags);
} }
await api.Gateway.Edit(repo.OwnerName, repo.Name, issueId, await api.Gateway.Edit(repo.OwnerName, repo.Name, (long) issueId,
new UpdateIssueRequest { Title = title, Description = description, Tag = tagString?.ToString()}); new UpdateIssueRequest { Title = title, Description = description, Tag = tagString?.ToString()});
return true; return true;
} }
@ -841,9 +837,9 @@ public class RepositoryService : BaseService<RepositoryService>
try try
{ {
var mergeChanges = method is RepositoryResetMethod.Soft; var mergeChanges = method is RepositoryResetMethod.Merge or RepositoryResetMethod.HardMerge;
var resetChanges = method is RepositoryResetMethod.Soft or RepositoryResetMethod.Hard; var resetChanges = method is RepositoryResetMethod.Soft or RepositoryResetMethod.Hard;
var hardMode = method is RepositoryResetMethod.Hard; var hardMode = method is RepositoryResetMethod.Hard or RepositoryResetMethod.HardMerge;
List<WorkspaceFile> unmerged = new(); List<WorkspaceFile> unmerged = new();
// Apply files excepts local changed. // Apply files excepts local changed.
@ -896,6 +892,7 @@ public class RepositoryService : BaseService<RepositoryService>
} }
catch (Exception exception) { Console.WriteLine(exception); } catch (Exception exception) { Console.WriteLine(exception); }
UIHelper.NotifyError(e); UIHelper.NotifyError(e);
Console.WriteLine(e); Console.WriteLine(e);
await DeleteFromDiskAsync(repo); await DeleteFromDiskAsync(repo);
@ -941,45 +938,47 @@ public class RepositoryService : BaseService<RepositoryService>
} }
} }
public async ValueTask<bool> MergeFilesAsync(RepositoryModel repo, RepositoryFileTreeAccessor repoAccessor, IEnumerable<WorkspaceFile> unmerged) public async ValueTask MergeFilesAsync(RepositoryModel repo, RepositoryFileTreeAccessor repoAccessor, IEnumerable<WorkspaceFile> unmerged)
{ {
var root = PathUtility.GetWorkspaceManagerPath(Api.C.Username.Value, repo.OwnerName, repo.Name); var root = PathUtility.GetWorkspaceManagerPath(Api.C.Username.Value, repo.OwnerName, repo.Name);
var tempFolder = Path.Combine(root, "MergeInTemp"); var srcTem = Path.Combine(root, "MergeInTemp");
var wsRoot = PathUtility.GetWorkspacePath(Api.C.Username.Value, repo.OwnerName, repo.Name);
var left = Path.Combine(tempFolder, "left");
var right = Path.Combine(tempFolder, "right");
using (Disposable.Create(tempFolder, r => Directory.Delete(r, true))) using (Disposable.Create(srcTem, r => Directory.Delete(r, true)))
{ {
Directory.CreateDirectory(root); Directory.CreateDirectory(root);
foreach (var p in unmerged) // Prepare server files
{ var wsRoot = PathUtility.GetWorkspacePath(Api.C.Username.Value, repo.OwnerName, repo.Name);
try var vm = new MergeDialogViewModel(repo, unmerged, srcTem, wsRoot);
{
// Prepare for left and right file
var wfs = WorkPath.ToPlatformPath(wsRoot, p.WorkPath);
File.Copy(wfs, left, true);
await using var rightFs = File.Create(right);
repoAccessor.TryWriteDataIntoDisk(p.WorkPath, rightFs, out _);
// Raise dialog window foreach (var f in vm.MergeFiles)
{
var pf = WorkPath.ToPlatformPath(f.WorkPath, vm.SrcFolder);
Directory.CreateDirectory(Path.GetDirectoryName(pf)!);
await using var fs = File.Create(pf);
repoAccessor.TryWriteDataIntoDisk(f.WorkPath, fs, out _);
}
// Raise merge window
var opt = UIHelper.DefaultOverlayDialogOptionsYesNo(); var opt = UIHelper.DefaultOverlayDialogOptionsYesNo();
opt.Buttons = DialogButton.None; opt.Buttons = DialogButton.OK;
opt.Title = "Merge files"; opt.Title = "Merge files";
opt.IsCloseButtonVisible = true; opt.FullScreen = true;
opt.IsCloseButtonVisible = false;
var vm = new MergeDialogViewModel(p.WorkPath, wfs, p.WorkPath, left, right);
await OverlayDialog.ShowModal<MergeDialogView, MergeDialogViewModel>(vm, AppDefaultValues.HostId, opt); await OverlayDialog.ShowModal<MergeDialogView, MergeDialogViewModel>(vm, AppDefaultValues.HostId, opt);
}
catch (Exception e)
{
Console.Error.WriteLine(e);
}
}
}
return true; // Rollback files
foreach (var f in vm.MergeFiles)
{
var copyFrom = WorkPath.ToPlatformPath(f.WorkPath, vm.TmpFolder);
if (!File.Exists(copyFrom)) continue; // If file existed, override it.
var copyTo = WorkPath.ToPlatformPath(f.WorkPath, vm.DstFolder);
Directory.CreateDirectory(Path.GetDirectoryName(copyTo)!);
File.Copy(copyFrom, copyTo, true);
}
}
} }
public async ValueTask<bool> UpdateCommitsHistoryFromServerAsync(RepositoryModel repo) public async ValueTask<bool> UpdateCommitsHistoryFromServerAsync(RepositoryModel repo)
@ -1294,7 +1293,6 @@ public class RepositoryService : BaseService<RepositoryService>
var accessor = await DownloadDepotsAndUseLocalCachesToGenerateRepositoryFileTreeAccessorFromServerAsync(repo, commitId); var accessor = await DownloadDepotsAndUseLocalCachesToGenerateRepositoryFileTreeAccessorFromServerAsync(repo, commitId);
if (accessor == null) return null; //todo this is a really fatal issue... if (accessor == null) return null; //todo this is a really fatal issue...
await accessor.CreateCacheAsync();
if (localDb.RepoAccessor != null) if (localDb.RepoAccessor != null)
{ {
try try
@ -1449,7 +1447,7 @@ public class RepositoryService : BaseService<RepositoryService>
wh.TargetUrl = newWh.TargetUrl; wh.TargetUrl = newWh.TargetUrl;
wh.Active = newWh.IsActive; wh.Active = newWh.IsActive;
wh.EventType = (WebhookEventType) newWh.EventType; wh.EventType = newWh.EventType;
} }
foreach (var wh in dict.Values) foreach (var wh in dict.Values)
@ -1460,7 +1458,7 @@ public class RepositoryService : BaseService<RepositoryService>
TargetUrl = wh.TargetUrl, TargetUrl = wh.TargetUrl,
Active = wh.IsActive, Active = wh.IsActive,
CreatedAt = wh.CreatedAt.UtcDateTime, CreatedAt = wh.CreatedAt.UtcDateTime,
EventType = (WebhookEventType) wh.EventType EventType = wh.EventType
}); });
} }
return true; return true;
@ -1488,7 +1486,7 @@ public class RepositoryService : BaseService<RepositoryService>
new WebhookCreateRequest { new WebhookCreateRequest {
TargetUrl = targetUrl, TargetUrl = targetUrl,
Secret = secret, Secret = secret,
EventType = (int) evt, EventType = evt,
}); });
return await UpdateWebhooksFromServerAsync(repo); return await UpdateWebhooksFromServerAsync(repo);

View File

@ -24,14 +24,14 @@ public partial class IssueDetailViewModel : RoutableViewModelBase
private readonly RepositoryModel _repo; private readonly RepositoryModel _repo;
public IssueDetailViewModel(IScreen screen, RepositoryModel repo, int issueId) : base(screen) public IssueDetailViewModel(IScreen screen, RepositoryModel repo, ulong issueId) : base(screen)
{ {
_repo = repo; _repo = repo;
_service = RepositoryService.Current; _service = RepositoryService.Current;
LoadDataAsync(repo, issueId, true); LoadDataAsync(repo, issueId, true);
} }
private async Task LoadDataAsync(RepositoryModel repo, int issueId, bool quitIfFailed) private async Task LoadDataAsync(RepositoryModel repo, ulong issueId, bool quitIfFailed)
{ {
using var _ = UIHelper.MakeLoading("Fetching comments from server..."); using var _ = UIHelper.MakeLoading("Fetching comments from server...");
if (!await _service.UpdateIssueDetailsFromServerAsync(repo, issueId, false)) if (!await _service.UpdateIssueDetailsFromServerAsync(repo, issueId, false))
@ -84,9 +84,9 @@ public partial class IssueDetailViewModel : RoutableViewModelBase
} }
else else
{ {
if (!await _service.AddCommentAsync(_repo, CurrentIssue.Id, NewComment, ReplyTo?.Id ?? null)) return; if (!await _service.AddCommentAsync(_repo, CurrentIssue.Id, NewComment, _replyTo?.Id ?? null)) return;
ReplyTo = null; _replyTo = null;
NewComment = string.Empty; NewComment = string.Empty;
} }
} }

View File

@ -1,6 +1,5 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.Diagnostics;
using System.IO; using System.IO;
using System.Threading.Tasks; using System.Threading.Tasks;
using Flawless.Abstraction; using Flawless.Abstraction;
@ -12,42 +11,34 @@ namespace Flawless.Client.ViewModels.ModalBox;
public partial class MergeDialogViewModel : ViewModelBase public partial class MergeDialogViewModel : ViewModelBase
{ {
[Reactive] private string _ws; public ObservableCollection<WorkspaceFile> MergeFiles { get; }
[Reactive] private string _final; public RepositoryModel Repository { get; }
[Reactive] private string _leftFile; // Local file public string SrcFolder { get; }
[Reactive] private string _rightFile; // Remote file public string DstFolder { get; }
public MergeDialogViewModel(string ws, string final, string fileWorkPath, string leftFile, string rightFile) public string TmpFolder { get; }
public MergeDialogViewModel(RepositoryModel repository, IEnumerable<WorkspaceFile> files,
string srcFolder, string dstFolder)
{ {
_ws = ws; Repository = repository;
_final = final; MergeFiles = new ObservableCollection<WorkspaceFile>(files);
_leftFile = leftFile; SrcFolder = srcFolder;
_rightFile = rightFile; DstFolder = dstFolder;
TmpFolder = Directory.CreateTempSubdirectory("Flawless_Merge").Name;
} }
[ReactiveCommand] [ReactiveCommand]
private async Task RaiseMergeToolsAsync(string fileWorkPath) private async Task RaiseMergeToolsAsync(string fileWorkPath)
{ {
var result = Process.Start($"meld \"{LeftFile}\" \"{RightFile}\""); var srcFile = WorkPath.ToPlatformPath(fileWorkPath, SrcFolder);
} var dstFile = Path.Combine(fileWorkPath, DstFolder);
var tmpFile = Path.Combine(fileWorkPath, TmpFolder);
[ReactiveCommand]
private Task UseLeftFileAsync()
{
return UseFileAsync(LeftFile);
}
[ReactiveCommand]
private Task UseRightFileAsync()
{
return UseFileAsync(RightFile);
}
private async Task UseFileAsync(string path)
{
File.Copy(path, _final, true);
} }
} }

View File

@ -1,5 +1,4 @@
using System; using Flawless.Client.Remote;
using Flawless.Client.Models;
using ReactiveUI; using ReactiveUI;
using ReactiveUI.SourceGenerators; using ReactiveUI.SourceGenerators;
@ -13,6 +12,4 @@ public partial class WebhookEditDialogViewModel : ReactiveObject
[Reactive] private WebhookEventType _eventType; [Reactive] private WebhookEventType _eventType;
[Reactive] private Type _eventTypeType = typeof(WebhookEventType);
} }

View File

@ -163,7 +163,7 @@ public partial class RepositoryViewModel : RoutableViewModelBase
[Reactive] private ISeries[] _byDay = [new ColumnSeries<DateTimePoint>()]; [Reactive] private ISeries[] _byDay = [new ColumnSeries<DateTimePoint>()];
public ICartesianAxis[] XAxesByDay { get; set; } = [ public ICartesianAxis[] XAxesByDay { get; set; } = [
new DateTimeAxis(TimeSpan.FromDays(1), date => date.ToString("MM/dd/yyyy")) new DateTimeAxis(TimeSpan.FromDays(1), date => date.ToString("MMMM dd"))
]; ];
private string _wsRoot; private string _wsRoot;
@ -588,9 +588,8 @@ public partial class RepositoryViewModel : RoutableViewModelBase
var manifest = await RepositoryService.C.CommitWorkspaceAsync(Repository, changes, LocalDatabase.CommitMessage!); var manifest = await RepositoryService.C.CommitWorkspaceAsync(Repository, changes, LocalDatabase.CommitMessage!);
if (manifest == null) return; if (manifest == null) return;
// LocalDatabase.LocalAccessor.SetBaseline(manifest.Value.FilePaths); LocalDatabase.LocalAccessor.SetBaseline(manifest.Value.FilePaths);
LocalDatabase.CommitMessage = string.Empty; LocalDatabase.CommitMessage = string.Empty;
await RepositoryService.C.UpdateCommitsHistoryFromServerAsync(Repository);
await DetectLocalChangesAsyncCommand.Execute(); await DetectLocalChangesAsyncCommand.Execute();
await RendererFileTreeAsync(); await RendererFileTreeAsync();
SyncCommitsFromRepository(); SyncCommitsFromRepository();
@ -819,7 +818,7 @@ public partial class RepositoryViewModel : RoutableViewModelBase
ByDay = [ ByDay = [
new ColumnSeries<DateTimePoint> new ColumnSeries<DateTimePoint>
{ {
Values = rsp.CommitByDay.Select(k => new DateTimePoint(k.Day.Date, k.Count)).ToList(), Values = rsp.CommitByDay.Select(k => new DateTimePoint(k.Day.LocalDateTime, k.Count)).ToList(),
} }
]; ];
} }

View File

@ -2,7 +2,6 @@
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.Diagnostics; using System.Diagnostics;
using System.Linq; using System.Linq;
using System.Reactive.Linq;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using DynamicData; using DynamicData;
@ -44,22 +43,18 @@ public partial class SettingViewModel : RoutableViewModelBase
[Reactive] private UserModel.SexType _gender; [Reactive] private UserModel.SexType _gender;
[Reactive] private Type _genderType = typeof(UserModel.SexType);
[Reactive] private string _bio; [Reactive] private string _bio;
[Reactive] private DateTimeOffset [Reactive] private DateTime
_logSearchFrom = DateTime.Now.AddDays(-1), _logSearchFrom = DateTime.Now.AddDays(-1),
_logSearchTo = DateTime.Now; _logSearchTo = DateTime.Now;
[Reactive] private int? _page = 1; [Reactive] private int _page = 1;
[Reactive] private int? _pageSize = 50; [Reactive] private int _pageSize = 50;
[Reactive] private Microsoft.Extensions.Logging.LogLevel _loglevel = Microsoft.Extensions.Logging.LogLevel.Information; [Reactive] private Microsoft.Extensions.Logging.LogLevel _loglevel = Microsoft.Extensions.Logging.LogLevel.Information;
[Reactive] private Type _loglevelType = typeof(Microsoft.Extensions.Logging.LogLevel);
[Reactive] private string _serverBlacklist, _serverWhitelist; [Reactive] private string _serverBlacklist, _serverWhitelist;
public SettingViewModel(IScreen hostScreen) : base(hostScreen) public SettingViewModel(IScreen hostScreen) : base(hostScreen)
@ -107,9 +102,8 @@ public partial class SettingViewModel : RoutableViewModelBase
await Api.C.Gateway.UpdateInfo(new UserInfoModifyResponse await Api.C.Gateway.UpdateInfo(new UserInfoModifyResponse
{ {
NickName = this.Nickname == LoginUser.Nickname ? null! : this.Nickname, NickName = this.Nickname == LoginUser.Nickname ? null! : this.Nickname,
Gender = (int) this.Gender, Gender = this.Gender == LoginUser.Sex ? null : (UserSex) this.Gender,
Bio = this.Bio == LoginUser.Bio ? null! : this.Bio, Bio = this.Bio == LoginUser.Bio ? null! : this.Bio
PublicEmail = false
}); });
if (Email != LoginUser.Email) if (Email != LoginUser.Email)
@ -120,9 +114,6 @@ public partial class SettingViewModel : RoutableViewModelBase
await Api.C.Gateway.UpdatePhone(new UserContactModifyResponse await Api.C.Gateway.UpdatePhone(new UserContactModifyResponse
{ Phone = PhoneNumber, }); { Phone = PhoneNumber, });
LoginUser.Bio = Bio;
LoginUser.Sex = Gender;
LoginUser.Email = Email;
UIHelper.NotifySuccess("Saved"); UIHelper.NotifySuccess("Saved");
} }
catch (Exception ex) catch (Exception ex)
@ -244,19 +235,15 @@ public partial class SettingViewModel : RoutableViewModelBase
Users.Clear(); Users.Clear();
foreach (var user in users) foreach (var user in users)
{ {
var m = new UserModel Users.Add(new UserModel
{ {
Username = user.Username, Username = user.Username,
Email = user.Email, Email = user.Email,
IsActive = user.IsActive, IsActive = user.IsActive,
IsAdmin = user.IsAdmin ?? false, IsAdmin = user.IsAdmin ?? false,
CanEdit = user.Username != Api.C.Username.Value! CanEdit = user.Username != Api.C.Username.Value!
}; });
if (m.CanEdit) Users.Insert(0, m);
else Users.Add(m);
} }
} }
catch (ApiException ex) catch (ApiException ex)
{ {
@ -367,12 +354,9 @@ public partial class SettingViewModel : RoutableViewModelBase
{ {
try try
{ {
Page ??= 1;
PageSize ??= 50;
var logs = await Api.C.Gateway.Logs( var logs = await Api.C.Gateway.Logs(
LogSearchFrom, LogSearchTo, (int)Loglevel, Page.Value, PageSize.Value); LogSearchFrom, LogSearchTo, (LogLevel?)Loglevel, Page, PageSize);
Logs.Clear();
Logs.AddRange(logs.Select(Log.From)); Logs.AddRange(logs.Select(Log.From));
} }
catch (Exception ex) catch (Exception ex)

View File

@ -6,15 +6,19 @@
xmlns:u="https://irihi.tech/ursa" xmlns:u="https://irihi.tech/ursa"
x:DataType="vm:MergeDialogViewModel" x:DataType="vm:MergeDialogViewModel"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
MinWidth="600" MinWidth="400"
x:Class="Flawless.Client.Views.ModalBox.MergeDialogView"> x:Class="Flawless.Client.Views.ModalBox.MergeDialogView">
<StackPanel Orientation="Vertical" HorizontalAlignment="Stretch" Spacing="12"> <ListBox HorizontalAlignment="Stretch" ItemsSource="{Binding MergeFiles}">
<Label Content="{Binding Ws, StringFormat='Conflict on file:\n {0}'}"/> <ListBox.ItemTemplate>
<Label Content="{Binding Ws, StringFormat='Left is local and right is remote'}"/> <DataTemplate>
<StackPanel Orientation="Horizontal" Spacing="4" HorizontalAlignment="Center"> <ListBoxItem>
<Button Content="Use Left" Command="{Binding UseLeftFileCommand}"/> <Grid ColumnDefinitions="Auto, *, Auto">
<Button Content="Merge" Command="{Binding RaiseMergeToolsCommand}"/> <Label Grid.Column="0" Content="{Binding WorkPath}"/>
<Button Content="Use Right" Command="{Binding UseRightFileCommand}"/> <u:IconButton Command="{Binding $parent[ItemsControl].((vm:MergeDialogViewModel)DataContext).RaiseMergeToolsCommand}"
</StackPanel> CommandParameter="{Binding WorkPath}"/>
</StackPanel> </Grid>
</ListBoxItem>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</UserControl> </UserControl>

View File

@ -22,7 +22,7 @@
<TextBox Text="{Binding Secret}"/> <TextBox Text="{Binding Secret}"/>
</u:FormItem> </u:FormItem>
<u:FormItem Grid.Row="1" Label="Event Type"> <u:FormItem Grid.Row="1" Label="Event Type">
<u:EnumSelector EnumType="{Binding EventTypeType}" Value="{Binding EventType}"/> <u:EnumSelector SelectedValue="{Binding EventType}"/>
</u:FormItem> </u:FormItem>
</u:Form> </u:Form>
</UserControl> </UserControl>

View File

@ -105,8 +105,7 @@
</StackPanel> </StackPanel>
<StackPanel HorizontalAlignment="Stretch" Orientation="Vertical"> <StackPanel HorizontalAlignment="Stretch" Orientation="Vertical">
<Label>Every Day Commits</Label> <Label>Every Day Commits</Label>
<lvc:CartesianChart MinHeight="280" Series="{Binding ByDay, Mode=TwoWay}" XAxes="{Binding XAxesByDay, Mode=TwoWay}"> <lvc:CartesianChart Series="{Binding ByDay}" XAxes="{Binding XAxesByDay}"/>
</lvc:CartesianChart>
</StackPanel> </StackPanel>
<StackPanel HorizontalAlignment="Stretch" Orientation="Vertical"> <StackPanel HorizontalAlignment="Stretch" Orientation="Vertical">
<Label>Depot Sizes</Label> <Label>Depot Sizes</Label>

View File

@ -25,8 +25,7 @@
<TextBox u:FormItem.Label="Nickname" Text="{Binding Nickname}"/> <TextBox u:FormItem.Label="Nickname" Text="{Binding Nickname}"/>
<TextBox u:FormItem.Label="Email" Text="{Binding Email}"/> <TextBox u:FormItem.Label="Email" Text="{Binding Email}"/>
<TextBox u:FormItem.Label="Phone Number" Text="{Binding PhoneNumber}"/> <TextBox u:FormItem.Label="Phone Number" Text="{Binding PhoneNumber}"/>
<u:EnumSelector u:FormItem.Label="Gender" EnumType="{Binding GenderType}" <u:EnumSelector u:FormItem.Label="Gender" SelectedValue="{Binding Gender}"/>
Value="{Binding Gender}"/>
<TextBox u:FormItem.Label="Bio" Classes="TextArea" Text="{Binding Bio}"/> <TextBox u:FormItem.Label="Bio" Classes="TextArea" Text="{Binding Bio}"/>
<StackPanel Orientation="Horizontal" Spacing="4"> <StackPanel Orientation="Horizontal" Spacing="4">
@ -37,7 +36,22 @@
</u:Form> </u:Form>
</ScrollViewer> </ScrollViewer>
</TabItem> </TabItem>
<TabItem Header="Preference">
<ScrollViewer Width="600" HorizontalAlignment="Left" Margin="6">
<u:Form HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
<!-- <u:FormItem Label="Default Storage Location"> -->
<!-- <u:PathPicker/> -->
<!-- </u:FormItem> -->
<u:FormGroup Header="Extern Tools">
<u:PathPicker u:FormItem.Label="Diff Tool"/>
</u:FormGroup>
<StackPanel Orientation="Horizontal" Spacing="4">
<u:IconButton Content="Save" Command="{Binding SaveClientPreferenceCommand}"/>
<u:IconButton Classes="Danger" Content="Reset" Command="{Binding ResetClientPreferenceCommand}"/>
</StackPanel>
</u:Form>
</ScrollViewer>
</TabItem>
<TabItem IsVisible="{Binding LoginUser.IsAdmin}" Header="Server"> <TabItem IsVisible="{Binding LoginUser.IsAdmin}" Header="Server">
<ScrollViewer Width="600" HorizontalAlignment="Left" Margin="6"> <ScrollViewer Width="600" HorizontalAlignment="Left" Margin="6">
<u:Form HorizontalAlignment="Stretch" VerticalAlignment="Stretch"> <u:Form HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
@ -124,19 +138,19 @@
<DatePicker SelectedDate="{Binding LogSearchTo}"/> <DatePicker SelectedDate="{Binding LogSearchTo}"/>
</StackPanel> </StackPanel>
<StackPanel Orientation="Horizontal" Spacing="4"> <StackPanel Orientation="Horizontal" Spacing="4">
<u:EnumSelector EnumType="{Binding LoglevelType}" Value="{Binding Loglevel, Mode=TwoWay}"/> <u:EnumSelector SelectedValue="{Binding Loglevel, Mode=TwoWay}"/>
<NumericUpDown Value="{Binding PageSize, Mode=TwoWay}" Minimum="10" Maximum="100"/> <NumericUpDown Value="{Binding PageSize, Mode=TwoWay}" Minimum="10" Maximum="100"/>
<NumericUpDown Value="{Binding Page, Mode=TwoWay}" Minimum="1"/> <NumericUpDown Value="{Binding Page, Mode=TwoWay}" Minimum="1"/>
<u:IconButton Icon="{StaticResource SemiIconSearch}" <u:IconButton Icon="{StaticResource SemiIconSearch}"
Command="{Binding DownloadServerLogCommand}"/> Command="{Binding DownloadServerLogCommand}"/>
</StackPanel> </StackPanel>
<ListBox HorizontalAlignment="Stretch" VerticalAlignment="Stretch" ItemsSource="{Binding Logs, Mode=TwoWay}"> <ListBox ItemsSource="{Binding Logs, Mode=TwoWay}">
<ListBox.ItemTemplate> <ListBox.ItemTemplate>
<DataTemplate> <DataTemplate>
<Grid RowDefinitions="Auto, *" ColumnDefinitions="Auto,*,Auto"> <Grid RowDefinitions="Auto, *" ColumnDefinitions="Auto,*,Auto">
<Label Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="3" Content="{Binding Message}"/> <Label Grid.Row="0" Grid.Column="0" FontSize="16" Content="{Binding Level}"/>
<Label Grid.Row="1" Grid.Column="0" FontSize="10" Content="{Binding Level}"/> <Label Grid.Row="0" Grid.Column="2" FontSize="16" Content="{Binding Time}"/>
<Label Grid.Row="1" Grid.Column="2" FontSize="10" Content="{Binding Time}"/> <Label Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="3" Content="{Binding Message}"/>
</Grid> </Grid>
</DataTemplate> </DataTemplate>
</ListBox.ItemTemplate> </ListBox.ItemTemplate>

View File

@ -1,8 +1,3 @@
namespace Flawless.Communication.Request; namespace Flawless.Communication.Request;
public struct AddCommentRequest public record AddCommentRequest(string Content, ulong? ReplyTo);
{
public string Content;
public int? ReplyTo;
}

View File

@ -2,11 +2,11 @@
namespace Flawless.Communication.Request; namespace Flawless.Communication.Request;
public struct UserInfoModifyResponse public record UserInfoModifyResponse
{ {
public string? NickName { get; set; } public string? NickName { get; set; }
public int Gender { get; set; } public UserSex? Gender { get; set; }
public string? Bio { get; set; } public string? Bio { get; set; }

View File

@ -2,4 +2,4 @@ using Flawless.Communication.Shared;
namespace Flawless.Communication.Request; namespace Flawless.Communication.Request;
public record WebhookCreateRequest(string TargetUrl, int EventType, string Secret); public record WebhookCreateRequest(string TargetUrl, WebhookEventType EventType, string Secret);

View File

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

View File

@ -4,7 +4,7 @@ namespace Flawless.Communication.Response;
public struct WebhookResponse public struct WebhookResponse
{ {
public WebhookResponse(int id, string targetUrl, int eventType, bool isActive, DateTime createdAt) public WebhookResponse(int id, string targetUrl, WebhookEventType eventType, bool isActive, DateTime createdAt)
{ {
Id = id; Id = id;
TargetUrl = targetUrl; TargetUrl = targetUrl;
@ -17,7 +17,7 @@ public struct WebhookResponse
public string TargetUrl { get; set; } public string TargetUrl { get; set; }
public int EventType { get; set; } public WebhookEventType EventType { get; set; }
public bool IsActive { get; set; } = true; public bool IsActive { get; set; } = true;

View File

@ -1,10 +1,10 @@
namespace Flawless.Communication.Shared; namespace Flawless.Communication.Shared;
public record IssueInfo( public record IssueInfo(
int Id, string Author, string Title, DateTime CreateAt, DateTime LastUpdate, bool closed, string? Tag); ulong Id, string Author, string Title, DateTime CreateAt, DateTime LastUpdate, bool closed, string? Tag);
public record IssueDetailInfo( public record IssueDetailInfo(
int Id, string Author, string Title, string Description, DateTime CreateAt, DateTime LastUpdate, bool closed, string? Tag); ulong Id, string Author, string Title, string Description, DateTime CreateAt, DateTime LastUpdate, bool closed, string? Tag);
public record IssueCommentInfo( public record IssueCommentInfo(
int Id, string Author, string Content, DateTime CreateAt, DateTime LastUpdate, ulong? ReplyTo); ulong Id, string Author, string Content, DateTime CreateAt, DateTime LastUpdate, ulong? ReplyTo);

View File

@ -32,14 +32,13 @@ public class AdminController(
UserName = request.Username, UserName = request.Username,
Email = request.Email, Email = request.Email,
EmailConfirmed = true, EmailConfirmed = true,
CreatedOn = DateTime.UtcNow CreatedOn = DateTime.UtcNow,
}; };
user.RenewSecurityStamp(); user.RenewSecurityStamp();
var result = await userManager.CreateAsync(user, request.Password); var result = await userManager.CreateAsync(user, request.Password);
if (result.Succeeded) if (result.Succeeded)
{ {
await userManager.SetLockoutEnabledAsync(user, false);
logger.LogInformation("User '{0}' created (SUPERUSER REGISTER)", user.UserName); logger.LogInformation("User '{0}' created (SUPERUSER REGISTER)", user.UserName);
return Ok(); return Ok();
} }
@ -205,7 +204,7 @@ public class AdminController(
public async Task<ActionResult<IEnumerable<LogEntryResponse>>> GetSystemLogsAsync( public async Task<ActionResult<IEnumerable<LogEntryResponse>>> GetSystemLogsAsync(
[FromQuery] DateTime startTime, [FromQuery] DateTime startTime,
[FromQuery] DateTime endTime, [FromQuery] DateTime endTime,
[FromQuery] int level, [FromQuery] LogLevel level,
[FromQuery] int page, [FromQuery] int page,
[FromQuery] int pageSize) [FromQuery] int pageSize)
{ {
@ -213,11 +212,11 @@ public class AdminController(
if (t != null) return t; if (t != null) return t;
var query = dbContext.SystemLogs.Where(x => var query = dbContext.SystemLogs.Where(x =>
x.Timestamp >= startTime && x.Timestamp <= endTime && (int) x.LogLevel >= level x.Timestamp >= startTime && x.Timestamp <= endTime && x.LogLevel >= level
).AsQueryable(); ).AsQueryable();
// 分页处理 // 分页处理
// var totalCount = await query.CountAsync(); var totalCount = await query.CountAsync();
var results = await query var results = await query
.OrderByDescending(l => l.Timestamp) .OrderByDescending(l => l.Timestamp)
.Skip((page - 1) * pageSize) .Skip((page - 1) * pageSize)
@ -229,6 +228,6 @@ public class AdminController(
l.Exception)) l.Exception))
.ToListAsync(); .ToListAsync();
return Ok(results); return Ok(new PaginatedResponse<LogEntryResponse>(results, totalCount, page, pageSize));
} }
} }

View File

@ -1,5 +1,4 @@
using System.Security.Claims; using System.Security.Claims;
using System.Text;
using Flawless.Communication.Request; using Flawless.Communication.Request;
using Flawless.Communication.Response; using Flawless.Communication.Response;
using Flawless.Communication.Shared; using Flawless.Communication.Shared;
@ -49,8 +48,7 @@ public class AuthenticationController(
Email = r.AdminEmail, Email = r.AdminEmail,
EmailConfirmed = true, EmailConfirmed = true,
CreatedOn = DateTime.UtcNow, CreatedOn = DateTime.UtcNow,
Admin = true, Admin = true
LockoutEnabled = false
}; };
user.RenewSecurityStamp(); user.RenewSecurityStamp();
@ -76,20 +74,19 @@ public class AuthenticationController(
UserName = request.Username, UserName = request.Username,
Email = request.Email, Email = request.Email,
EmailConfirmed = true, EmailConfirmed = true,
CreatedOn = DateTime.UtcNow CreatedOn = DateTime.UtcNow,
}; };
user.RenewSecurityStamp(); user.RenewSecurityStamp();
var result = await userManager.CreateAsync(user, request.Password); var result = await userManager.CreateAsync(user, request.Password);
if (result.Succeeded) if (result.Succeeded)
{ {
await userManager.SetLockoutEnabledAsync(user, false);
logger.LogInformation("User '{0}' created (PUBLIC REGISTER)", user.UserName); logger.LogInformation("User '{0}' created (PUBLIC REGISTER)", user.UserName);
return Ok(); return Ok();
} }
logger.LogInformation("User '{0}' NOT created (PUBLIC REGISTER) : {1}", user.UserName, result.Errors); logger.LogInformation("User '{0}' NOT created (PUBLIC REGISTER) : {1}", user.UserName, result.Errors);
return BadRequest(new FailedResponse(new StringBuilder().AppendJoin(", ", result.Errors).ToString())); return BadRequest(new FailedResponse(result.Errors));
} }
[HttpPost("login")] [HttpPost("login")]
@ -98,7 +95,6 @@ public class AuthenticationController(
var user = await userManager.FindByNameAsync(r.Username); var user = await userManager.FindByNameAsync(r.Username);
if (user == null) return BadRequest(new FailedResponse("Invalid username or password.")); if (user == null) return BadRequest(new FailedResponse("Invalid username or password."));
if (user.LockoutEnabled) return BadRequest(new FailedResponse("Account is locked out."));
var result = await signInManager.CheckPasswordSignInAsync(user, r.Password, false); var result = await signInManager.CheckPasswordSignInAsync(user, r.Password, false);
if (result.Succeeded) if (result.Succeeded)
{ {
@ -136,7 +132,6 @@ public class AuthenticationController(
var principal = tokenService.GetPrincipalFromExpiredToken(r.Token); var principal = tokenService.GetPrincipalFromExpiredToken(r.Token);
var user = await userManager.GetUserAsync(principal); var user = await userManager.GetUserAsync(principal);
if (user == null) return BadRequest(new FailedResponse("Token is ban. Please login again.")); if (user == null) return BadRequest(new FailedResponse("Token is ban. Please login again."));
if (user.LockoutEnabled) return BadRequest(new FailedResponse("Account is locked out."));
try try
{ {

View File

@ -51,7 +51,7 @@ public class IssueController(
public async Task<IActionResult> AddCommentAsync( public async Task<IActionResult> AddCommentAsync(
string userName, string userName,
string repositoryName, string repositoryName,
int issueId, ulong issueId,
[FromBody] AddCommentRequest request) [FromBody] AddCommentRequest request)
{ {
var user = (await userManager.GetUserAsync(HttpContext.User))!; var user = (await userManager.GetUserAsync(HttpContext.User))!;
@ -122,7 +122,7 @@ public class IssueController(
public async Task<ActionResult<IssueDetailInfo>> GetIssueDetailsAsync( public async Task<ActionResult<IssueDetailInfo>> GetIssueDetailsAsync(
string userName, string userName,
string repositoryName, string repositoryName,
int issueId) ulong issueId)
{ {
var user = (await userManager.GetUserAsync(HttpContext.User))!; var user = (await userManager.GetUserAsync(HttpContext.User))!;
var grantIssue = await ValidateRepositoryAsync(userName, repositoryName, user, RepositoryRole.Reporter); var grantIssue = await ValidateRepositoryAsync(userName, repositoryName, user, RepositoryRole.Reporter);
@ -152,7 +152,7 @@ public class IssueController(
public async Task<IActionResult> UpdateIssueAsync( public async Task<IActionResult> UpdateIssueAsync(
string userName, string userName,
string repositoryName, string repositoryName,
int issueId, ulong issueId,
[FromBody] UpdateIssueRequest request) [FromBody] UpdateIssueRequest request)
{ {
var user = (await userManager.GetUserAsync(HttpContext.User))!; var user = (await userManager.GetUserAsync(HttpContext.User))!;
@ -198,7 +198,7 @@ public class IssueController(
public async Task<ActionResult<ListingResponse<CommentResponse>>> GetIssueCommentsAsync( public async Task<ActionResult<ListingResponse<CommentResponse>>> GetIssueCommentsAsync(
string userName, string userName,
string repositoryName, string repositoryName,
int issueId) ulong issueId)
{ {
var user = (await userManager.GetUserAsync(HttpContext.User))!; var user = (await userManager.GetUserAsync(HttpContext.User))!;
var grantIssue = await ValidateRepositoryAsync(userName, repositoryName, user, RepositoryRole.Reporter); var grantIssue = await ValidateRepositoryAsync(userName, repositoryName, user, RepositoryRole.Reporter);

View File

@ -180,15 +180,10 @@ public class RepositoryInnieController(
{ {
var user = (await userManager.GetUserAsync(HttpContext.User))!; var user = (await userManager.GetUserAsync(HttpContext.User))!;
var grantIssue = await ValidateRepositoryAsync(userName, repositoryName, user, RepositoryRole.Owner); var grantIssue = await ValidateRepositoryAsync(userName, repositoryName, user, RepositoryRole.Owner);
if (grantIssue is not Repository _) return (ActionResult) grantIssue; if (grantIssue is not Repository rp) return (ActionResult) grantIssue;
var response = new RepoStatisticResponse(); var response = new RepoStatisticResponse();
var rp = await dbContext.Repositories
.Include(r => r.Members)
.ThenInclude(m => m.User)
.Include(r => r.Owner).Include(repository => repository.Depots).Include(repository => repository.Commits)
.ThenInclude(repositoryCommit => repositoryCommit.Author)
.FirstAsync(r => r.Owner.UserName == userName && r.Name == repositoryName);
// 获取Depot大小统计 // 获取Depot大小统计
response.Depots = rp.Depots response.Depots = rp.Depots
@ -209,7 +204,7 @@ public class RepositoryInnieController(
// 获取每日提交数量统计 // 获取每日提交数量统计
response.CommitByDay = rp.Commits response.CommitByDay = rp.Commits
.GroupBy(c => c.CommittedOn.Date) .GroupBy(c => c.CommittedOn)
.Select(g => new RepoStatisticResponse.CommitByDayDetail .Select(g => new RepoStatisticResponse.CommitByDayDetail
{ {
Day = g.Key, Day = g.Key,
@ -229,7 +224,7 @@ public class RepositoryInnieController(
var grantIssue = await ValidateRepositoryAsync(userName, repositoryName, user, RepositoryRole.Owner); var grantIssue = await ValidateRepositoryAsync(userName, repositoryName, user, RepositoryRole.Owner);
if (grantIssue is not Repository rp) return (IActionResult) grantIssue; if (grantIssue is not Repository rp) return (IActionResult) grantIssue;
await webhookService.AddWebhookAsync(rp, request.TargetUrl, (WebhookEventType) request.EventType, request.Secret); await webhookService.AddWebhookAsync(rp, request.TargetUrl, request.EventType, request.Secret);
return Created(); return Created();
} }
@ -241,7 +236,7 @@ public class RepositoryInnieController(
if (grantIssue is not Repository rp) return (ActionResult) grantIssue; if (grantIssue is not Repository rp) return (ActionResult) grantIssue;
return Ok((await webhookService.GetWebhooksAsync(rp)).Select(x => new WebhookResponse( return Ok((await webhookService.GetWebhooksAsync(rp)).Select(x => new WebhookResponse(
x.Id, x.TargetUrl, (int) x.EventType, x.IsActive, x.CreatedAt))); x.Id, x.TargetUrl, x.EventType, x.IsActive, x.CreatedAt)));
} }
[HttpPost("webhooks/{webhookId}/toggle")] [HttpPost("webhooks/{webhookId}/toggle")]

View File

@ -24,30 +24,31 @@ public class UserController(
// Modify content // Modify content
var u = (await userManager.GetUserAsync(HttpContext.User))!; var u = (await userManager.GetUserAsync(HttpContext.User))!;
if (r.NickName != u.NickName) if (r.NickName != null)
{ {
update = true; update = true;
u.NickName = r.NickName; u.NickName = r.NickName;
} }
if (r.Bio != u.Bio) if (r.Bio != null)
{ {
update = true; update = true;
u.Bio = r.Bio; u.Bio = r.Bio;
} }
if (r.Gender != (int) u.Gender) if (r.Gender != null)
{ {
update = true; update = true;
u.Gender = (UserSex) r.Gender; u.Gender = r.Gender ?? UserSex.Unset;
} }
if (r.PublicEmail != u.PublicEmail) if (r.PublicEmail != null)
{ {
update = true; update = true;
u.PublicEmail = r.PublicEmail ?? false; u.PublicEmail = r.PublicEmail ?? false;
} }
if (renew) u.RenewSecurityStamp(); if (renew) u.RenewSecurityStamp();
if (update || renew) await userManager.UpdateAsync(u); if (update || renew) await userManager.UpdateAsync(u);
return Ok(); return Ok();

View File

@ -1,12 +1,11 @@
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace Flawless.Server.Models; namespace Flawless.Server.Models;
public class RepositoryIssue public class RepositoryIssue
{ {
[Key] [Key, Required]
public int Id { get; set; } public ulong Id { get; set; }
[Required] [Required]
public required Repository Repository { get; set; } public required Repository Repository { get; set; }
@ -32,8 +31,8 @@ public class RepositoryIssue
public class RepositoryIssueContent public class RepositoryIssueContent
{ {
[Key] [Key, Required]
public int Id { get; set; } public ulong Id { get; set; }
[Required] [Required]
public RepositoryIssue Issue { get; set; } public RepositoryIssue Issue { get; set; }

View File

@ -202,7 +202,7 @@ public static class Program
if (u == null) throw new SecurityTokenExpiredException("User is not existed."); if (u == null) throw new SecurityTokenExpiredException("User is not existed.");
if (u.SecurityStamp != stamp) throw new SecurityTokenExpiredException("SecurityStamp is mismatched."); if (u.SecurityStamp != stamp) throw new SecurityTokenExpiredException("SecurityStamp is mismatched.");
if (u.LockoutEnabled) throw new SecurityTokenExpiredException("User has been locked."); // if (u.LockoutEnabled) throw new SecurityTokenExpiredException("User has been locked."); //todo Fix lockout prob
} }
// Extract user info into HttpContext // Extract user info into HttpContext