diff --git a/.idea/.idea.Flawless-Version-Control/.idea/avalonia.xml b/.idea/.idea.Flawless-Version-Control/.idea/avalonia.xml index ba59aa9..ca6ba04 100644 --- a/.idea/.idea.Flawless-Version-Control/.idea/avalonia.xml +++ b/.idea/.idea.Flawless-Version-Control/.idea/avalonia.xml @@ -12,6 +12,7 @@ + @@ -21,6 +22,7 @@ + diff --git a/Flawless.Client/Service/RepositoryService.cs b/Flawless.Client/Service/RepositoryService.cs index 61adb88..d1b5e96 100644 --- a/Flawless.Client/Service/RepositoryService.cs +++ b/Flawless.Client/Service/RepositoryService.cs @@ -346,7 +346,7 @@ public class RepositoryService : BaseService return true; } - public async ValueTask UpdateIssueDetailsFromServerAsync(RepositoryModel repo, ulong issueId) + public async ValueTask UpdateIssueDetailsFromServerAsync(RepositoryModel repo, ulong issueId, bool titleOnly) { var api = Api.C; try @@ -358,8 +358,6 @@ public class RepositoryService : BaseService } 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); @@ -380,14 +378,20 @@ public class RepositoryService : BaseService entity.Tags.AddRange(tags); repo.Issues.Add(entity); - entity.Comments.AddRange(details.Select(x => new RepositoryModel.Comment + if (!titleOnly) { - 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 - })); + var details = (await api.Gateway.Comments(repo.Name, repo.OwnerName, (long)issueId)) + .Result.ToImmutableDictionary(x => x.CommentId); + + 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 { @@ -398,35 +402,41 @@ public class RepositoryService : BaseService entity.Tags.Clear(); entity.Tags.AddRange(tags); - - for (var i = 0; i < entity.Comments.Count; i++) + + if (!titleOnly) { - 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 details = (await api.Gateway.Comments(repo.Name, repo.OwnerName, (long)issueId)) + .Result.ToImmutableDictionary(x => x.CommentId); + + for (var i = 0; i < entity.Comments.Count; i++) { - 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); + var c = entity.Comments[i]; + if (!details.ContainsKey((long) c.Id)) repo.Issues.RemoveAt(i); } - else + + foreach (var (key, value) in details) { - d.Author = value.Author; - d.Content = value.Content; - d.CreatedAt = value.CreatedAt.UtcDateTime; - d.ReplyTo = value.ReplyToId.HasValue ? (ulong)value.ReplyToId.Value : null; + 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; + } } } } @@ -443,7 +453,7 @@ public class RepositoryService : BaseService return true; } - public async ValueTask CreateIssueAsync(RepositoryModel repo, string title, string description, IEnumerable? tags) + public async ValueTask CreateIssueAsync(RepositoryModel repo, string title, string description, IEnumerable? tags) { var api = Api.C; try @@ -460,10 +470,27 @@ public class RepositoryService : BaseService new StringBuilder().AppendJoin(',', tags); } - return await api.Gateway.Create( + var issue = await api.Gateway.Create( repo.OwnerName, repo.Name, new CreateIssueRequest { Title = title, Description = description, Tag = tagString?.ToString()}); + + var 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); + + repo.Issues.Sort((x, y) => (int) ((long) x.Id - (long) y.Id)); + return entity.Id; } catch (Exception e) { @@ -485,6 +512,7 @@ public class RepositoryService : BaseService } await api.Gateway.Close(repo.OwnerName, repo.Name, (long) issueId); + await UpdateIssueDetailsFromServerAsync(repo, issueId, true); return true; } catch (Exception e) @@ -507,6 +535,7 @@ public class RepositoryService : BaseService } await api.Gateway.Reopen(repo.OwnerName, repo.Name, (long) issueId); + await UpdateIssueDetailsFromServerAsync(repo, issueId, true); return true; } catch (Exception e) @@ -540,7 +569,7 @@ public class RepositoryService : BaseService } } - public async ValueTask UpdateIssueAsync(RepositoryModel repo, long issueId, string? title, string? description, IEnumerable? tags) + public async ValueTask UpdateIssueAsync(RepositoryModel repo, ulong issueId, string? title, string? description, IEnumerable? tags) { var api = Api.C; try @@ -557,7 +586,7 @@ public class RepositoryService : BaseService 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()}); return true; } diff --git a/Flawless.Client/UIHelper.cs b/Flawless.Client/UIHelper.cs index 693981f..78c4150 100644 --- a/Flawless.Client/UIHelper.cs +++ b/Flawless.Client/UIHelper.cs @@ -99,7 +99,7 @@ public static class UIHelper } } - public static OverlayDialogOptions DefaultOverlayDialogOptions() + public static OverlayDialogOptions DefaultOverlayDialogOptionsYesNo() { return new OverlayDialogOptions { diff --git a/Flawless.Client/ViewModels/IssueDetailViewModel.cs b/Flawless.Client/ViewModels/IssueDetailViewModel.cs index d4bfac6..e5ba4cf 100644 --- a/Flawless.Client/ViewModels/IssueDetailViewModel.cs +++ b/Flawless.Client/ViewModels/IssueDetailViewModel.cs @@ -1,3 +1,4 @@ +using System; using System.Linq; using System.Reactive.Threading.Tasks; using System.Threading.Tasks; @@ -6,7 +7,10 @@ using Flawless.Client.Models; using ReactiveUI; using Flawless.Client.Service; using Flawless.Client.ViewModels; +using Flawless.Client.ViewModels.ModalBox; +using Flawless.Client.Views.ModalBox; using ReactiveUI.SourceGenerators; +using Ursa.Controls; public partial class IssueDetailViewModel : RoutableViewModelBase { @@ -24,16 +28,16 @@ public partial class IssueDetailViewModel : RoutableViewModelBase { _repo = repo; _service = RepositoryService.Current; - LoadDataAsync(repo, issueId); + LoadDataAsync(repo, issueId, true); } - private async Task LoadDataAsync(RepositoryModel repo, ulong issueId) + private async Task LoadDataAsync(RepositoryModel repo, ulong issueId, bool quitIfFailed) { using var _ = UIHelper.MakeLoading("Fetching comments from server..."); - if (!await _service.UpdateIssueDetailsFromServerAsync(repo, issueId)) + if (!await _service.UpdateIssueDetailsFromServerAsync(repo, issueId, false)) { - await NavigateBackAsync(); - UIHelper.NotifyError("Operation failed", "Can not open issue..."); + if (quitIfFailed) await NavigateBackAsync(); + UIHelper.NotifyError("Operation failed", "Can not load issue..."); return; } @@ -41,7 +45,7 @@ public partial class IssueDetailViewModel : RoutableViewModelBase } [ReactiveCommand] - private Task RefreshIssueAsync() => LoadDataAsync(_repo, CurrentIssue!.Id); + private Task RefreshIssueAsync() => LoadDataAsync(_repo, CurrentIssue!.Id, false); [ReactiveCommand] private async Task ToggleIssueStatusAsync() @@ -52,7 +56,6 @@ public partial class IssueDetailViewModel : RoutableViewModelBase if (CurrentIssue.Closed) { if (!await _service.ReopenIssueAsync(_repo, CurrentIssue.Id)) return; - } else { @@ -62,6 +65,13 @@ public partial class IssueDetailViewModel : RoutableViewModelBase CurrentIssue.Closed = !CurrentIssue.Closed; } + [ReactiveCommand] + private void MarkAsReplayTo(RepositoryModel.Comment? comment) + { + if (CurrentIssue == null) return; + ReplyTo = comment; + } + [ReactiveCommand] private async Task AddCommentAsync() { @@ -80,10 +90,30 @@ public partial class IssueDetailViewModel : RoutableViewModelBase NewComment = string.Empty; } } - + [ReactiveCommand] private async Task EditIssueAsync() { + if (CurrentIssue == null) return; + + var opt = UIHelper.DefaultOverlayDialogOptionsYesNo(); + var vm = new IssueEditDialogViewModel(CurrentIssue); + + var result = await OverlayDialog.ShowModal( + vm, AppDefaultValues.HostId, opt); + + if (result == DialogResult.Yes) + { + using var l = UIHelper.MakeLoading("Updating issue..."); + var tags = vm.Tag?.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + + // 调用服务端更新接口 + await RepositoryService.C.UpdateIssueAsync( + _repo, CurrentIssue.Id, + vm.Title, vm.Description, tags + ); + + } } [ReactiveCommand] diff --git a/Flawless.Client/ViewModels/RepositoryViewModel.cs b/Flawless.Client/ViewModels/RepositoryViewModel.cs index 4b34584..8f1276d 100644 --- a/Flawless.Client/ViewModels/RepositoryViewModel.cs +++ b/Flawless.Client/ViewModels/RepositoryViewModel.cs @@ -422,25 +422,40 @@ public partial class RepositoryViewModel : RoutableViewModelBase [ReactiveCommand] private async Task CreateIssueAsync() { - + var opt = UIHelper.DefaultOverlayDialogOptionsYesNo(); + var vm = new IssueEditDialogViewModel(); + + var r = await OverlayDialog.ShowModal(vm, AppDefaultValues.HostId, opt); + if (r == DialogResult.Yes) + { + using var l = UIHelper.MakeLoading("Sending issue..."); + var tags = vm.Tag?.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + var issueId = await RepositoryService.C.CreateIssueAsync(Repository, vm.Title, vm.Description, tags); + if (issueId == null) return; + + await HostScreen.Router.Navigate.Execute(new IssueDetailViewModel(HostScreen, Repository, issueId.Value)); + } } [ReactiveCommand] - private async Task OpenIssueAsync() + private async Task OpenIssueAsync(RepositoryModel.Issue issue) { - + using var l = UIHelper.MakeLoading("Config issue..."); + await HostScreen.Router.Navigate.Execute(new IssueDetailViewModel(HostScreen, Repository, issue.Id)); } [ReactiveCommand] - private async Task CloseIssueAsync() + private async Task CloseIssueAsync(RepositoryModel.Issue issue) { - + using var l = UIHelper.MakeLoading("Config issue..."); + await RepositoryService.C.CloseIssueAsync(Repository, issue.Id); } [ReactiveCommand] - private async Task ReopenIssueAsync() + private async Task ReopenIssueAsync(RepositoryModel.Issue issue) { - + using var l = UIHelper.MakeLoading("Config issue..."); + await RepositoryService.C.ReopenIssueAsync(Repository, issue.Id); } [ReactiveCommand] @@ -553,6 +568,13 @@ public partial class RepositoryViewModel : RoutableViewModelBase UpdatePermissionOfRepository(); } + [ReactiveCommand] + private async ValueTask RefreshRepositoryIssuesAsync() + { + using var l = UIHelper.MakeLoading("Refreshing issues..."); + await RepositoryService.C.UpdateIssuesListFromServerAsync(Repository); + } + [ReactiveCommand] private async ValueTask DeleteMemberFromServerAsync(RepositoryModel.Member member) { @@ -581,7 +603,7 @@ public partial class RepositoryViewModel : RoutableViewModelBase return; } - var style = UIHelper.DefaultOverlayDialogOptions(); + var style = UIHelper.DefaultOverlayDialogOptionsYesNo(); var vm = new EditRepositoryMemberDialogViewModel(); vm.LockUsername = true; vm.Username = member.Username; @@ -618,7 +640,7 @@ public partial class RepositoryViewModel : RoutableViewModelBase return; } - var style = UIHelper.DefaultOverlayDialogOptions(); + var style = UIHelper.DefaultOverlayDialogOptionsYesNo(); var vm = new EditRepositoryMemberDialogViewModel(); var result = await OverlayDialog.ShowModal (vm, AppDefaultValues.HostId, style); diff --git a/Flawless.Client/Views/IssueDetailView.axaml b/Flawless.Client/Views/IssueDetailView.axaml index 0c098b1..a74315d 100644 --- a/Flawless.Client/Views/IssueDetailView.axaml +++ b/Flawless.Client/Views/IssueDetailView.axaml @@ -5,7 +5,8 @@ xmlns:vm="using:Flawless.Client.ViewModels" xmlns:u="https://irihi.tech/ursa" xmlns:global="clr-namespace:" - x:DataType="global:IssueDetailViewModel"> + x:DataType="global:IssueDetailViewModel" + x:Class="Flawless.Client.Views.IssueDetailView"> diff --git a/Flawless.Client/Views/IssueDetailView.axaml.cs b/Flawless.Client/Views/IssueDetailView.axaml.cs new file mode 100644 index 0000000..9865028 --- /dev/null +++ b/Flawless.Client/Views/IssueDetailView.axaml.cs @@ -0,0 +1,14 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; +using Ursa.ReactiveUIExtension; + +namespace Flawless.Client.Views; + +public partial class IssueDetailView : ReactiveUrsaView +{ + public IssueDetailView() + { + InitializeComponent(); + } +} \ No newline at end of file diff --git a/Flawless.Client/Views/ModalBox/IssueEditDialogView.axaml b/Flawless.Client/Views/ModalBox/IssueDetailEditView.axaml similarity index 78% rename from Flawless.Client/Views/ModalBox/IssueEditDialogView.axaml rename to Flawless.Client/Views/ModalBox/IssueDetailEditView.axaml index 463bb25..ec32514 100644 --- a/Flawless.Client/Views/ModalBox/IssueEditDialogView.axaml +++ b/Flawless.Client/Views/ModalBox/IssueDetailEditView.axaml @@ -2,11 +2,11 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" - xmlns:vm="using:Flawless.Client.ViewModels.ModalBox" xmlns:u="https://irihi.tech/ursa" + xmlns:vm="clr-namespace:Flawless.Client.ViewModels.ModalBox" + mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:DataType="vm:IssueEditDialogViewModel" - mc:Ignorable="d" - x:Class="Flawless.Client.Views.RepositoryPage.IssueEditDialogView"> + x:Class="Flawless.Client.Views.ModalBox.IssueDetailEditView"> @@ -22,4 +22,5 @@ SelectedItem="{Binding Tag}"/> - \ No newline at end of file + + diff --git a/Flawless.Client/Views/ModalBox/IssueDetailEditView.axaml.cs b/Flawless.Client/Views/ModalBox/IssueDetailEditView.axaml.cs new file mode 100644 index 0000000..e593858 --- /dev/null +++ b/Flawless.Client/Views/ModalBox/IssueDetailEditView.axaml.cs @@ -0,0 +1,13 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; + +namespace Flawless.Client.Views.ModalBox; + +public partial class IssueDetailEditView : UserControl +{ + public IssueDetailEditView() + { + InitializeComponent(); + } +} \ No newline at end of file diff --git a/Flawless.Client/Views/RepositoryPage/RepoIssuePageView.axaml b/Flawless.Client/Views/RepositoryPage/RepoIssuePageView.axaml index de50354..21727bc 100644 --- a/Flawless.Client/Views/RepositoryPage/RepoIssuePageView.axaml +++ b/Flawless.Client/Views/RepositoryPage/RepoIssuePageView.axaml @@ -10,11 +10,13 @@ + - + @@ -41,17 +43,15 @@ Foreground="{DynamicResource SemiSecondaryTextColor}"/> - - - - - - - - - - - + + + + + + + +