diff --git a/Flawless.Client/Models/RepositoryModel.cs b/Flawless.Client/Models/RepositoryModel.cs index 88164b6..02e95af 100644 --- a/Flawless.Client/Models/RepositoryModel.cs +++ b/Flawless.Client/Models/RepositoryModel.cs @@ -31,10 +31,7 @@ public partial class RepositoryModel : ReactiveModel public ObservableCollection Commits { get; } = new(); - // todo cache depots? - public ObservableCollection Depots { get; } = new(); - - public ObservableCollection Locks { get; } = new(); + public ObservableCollection Webhooks { get; } = new(); public ObservableCollection Issues { get; } = new(); @@ -45,7 +42,20 @@ public partial class RepositoryModel : ReactiveModel Developer = 2, Owner = 3, } - + + public partial class Webhook : ReactiveModel + { + [Reactive] private int _id; + + [Reactive] private string _targetUrl; + + [Reactive] private bool _active; + + [Reactive] private DateTime _createdAt; + + [Reactive] private WebhookEventType _eventType; + } + public partial class Issue : ReactiveModel { [Reactive] private ulong _id; diff --git a/Flawless.Client/Service/RepositoryService.cs b/Flawless.Client/Service/RepositoryService.cs index 7588f2f..0b63be8 100644 --- a/Flawless.Client/Service/RepositoryService.cs +++ b/Flawless.Client/Service/RepositoryService.cs @@ -572,6 +572,8 @@ public class RepositoryService : BaseService return false; } } + + public async ValueTask UpdateIssueAsync(RepositoryModel repo, ulong issueId, string? title, string? description, IEnumerable? tags) { @@ -1382,4 +1384,126 @@ public class RepositoryService : BaseService return depotFile; } + + public async ValueTask UpdateWebhooksFromServerAsync(RepositoryModel repo) + { + var api = Api.C; + try + { + if (api.RequireRefreshToken() && !(await api.TryRefreshTokenAsync())) + { + api.ClearGateway(); + return false; + } + + var webhooks = await api.Gateway.WebhooksGet(repo.OwnerName, repo.Name); + + var dict = webhooks.ToDictionary(w => w.Id); + for (var i = 0; i < repo.Webhooks.Count; i++) + { + var wh = repo.Webhooks[i]; + if (!dict.Remove(wh.Id, out var newWh)) + { + repo.Webhooks.RemoveAt(i); + i -= 1; + continue; + } + + wh.TargetUrl = newWh.TargetUrl; + wh.Active = newWh.IsActive; + wh.EventType = newWh.EventType; + } + + foreach (var wh in dict.Values) + { + repo.Webhooks.Add(new RepositoryModel.Webhook + { + Id = wh.Id, + TargetUrl = wh.TargetUrl, + Active = wh.IsActive, + CreatedAt = wh.CreatedAt.UtcDateTime, + EventType = wh.EventType + }); + } + return true; + } + catch (Exception e) + { + UIHelper.NotifyError(e); + Console.WriteLine(e); + return false; + } + } + + public async ValueTask AddWebhookAsync(RepositoryModel repo, string targetUrl, string secret, WebhookEventType evt) + { + var api = Api.C; + try + { + if (api.RequireRefreshToken() && !(await api.TryRefreshTokenAsync())) + { + api.ClearGateway(); + return false; + } + + await api.Gateway.Create(repo.OwnerName, repo.Name, + new WebhookCreateRequest { + TargetUrl = targetUrl, + Secret = secret, + EventType = evt, + }); + + return await UpdateWebhooksFromServerAsync(repo); + } + catch (Exception e) + { + UIHelper.NotifyError(e); + Console.WriteLine(e); + return false; + } + } + + public async ValueTask DeleteWebhookAsync(RepositoryModel repo, int webhookId) + { + var api = Api.C; + try + { + if (api.RequireRefreshToken() && !(await api.TryRefreshTokenAsync())) + { + api.ClearGateway(); + return false; + } + + await api.Gateway.WebhooksDelete(repo.OwnerName, repo.Name, webhookId); + return await UpdateWebhooksFromServerAsync(repo); + } + catch (Exception e) + { + UIHelper.NotifyError(e); + Console.WriteLine(e); + return false; + } + } + + public async ValueTask ToggleWebhookAsync(RepositoryModel repo, int webhookId, bool activate) + { + var api = Api.C; + try + { + if (api.RequireRefreshToken() && !(await api.TryRefreshTokenAsync())) + { + api.ClearGateway(); + return false; + } + + await api.Gateway.Toggle(repo.OwnerName, repo.Name, webhookId, activate); + return await UpdateWebhooksFromServerAsync(repo); + } + catch (Exception e) + { + UIHelper.NotifyError(e); + Console.WriteLine(e); + return false; + } + } } \ No newline at end of file diff --git a/Flawless.Client/ViewModels/ModalBox/WebhookEditDialogViewModel.cs b/Flawless.Client/ViewModels/ModalBox/WebhookEditDialogViewModel.cs new file mode 100644 index 0000000..0348d8f --- /dev/null +++ b/Flawless.Client/ViewModels/ModalBox/WebhookEditDialogViewModel.cs @@ -0,0 +1,15 @@ +using Flawless.Client.Remote; +using ReactiveUI; +using ReactiveUI.SourceGenerators; + +namespace Flawless.Client.ViewModels.ModalBox; + +public partial class WebhookEditDialogViewModel : ReactiveObject +{ + [Reactive] private string _url; + + [Reactive] private string _secret; + + [Reactive] private WebhookEventType _eventType; + +} diff --git a/Flawless.Client/ViewModels/RepositoryViewModel.cs b/Flawless.Client/ViewModels/RepositoryViewModel.cs index bb03ff4..5ae04b4 100644 --- a/Flawless.Client/ViewModels/RepositoryViewModel.cs +++ b/Flawless.Client/ViewModels/RepositoryViewModel.cs @@ -137,11 +137,9 @@ public partial class RepositoryViewModel : RoutableViewModelBase public UserModel User { get; } - public ServerStatusResponse ServerStatus => Api.C.Status.Value!; - [Reactive] private bool _autoDetectChanges = true; - [Reactive] private bool _isOwnerRole, _isDeveloperRole, _isReporterRole, _isGuestRole; + [Reactive] private bool _isOwnerRole, _isDeveloperRole, _isReporterRole, _isGuestRole, _showWebHook; private string _wsRoot; @@ -241,10 +239,13 @@ public partial class RepositoryViewModel : RoutableViewModelBase var path = Path.Combine(_wsRoot, node.FullPath); return (ulong)new FileInfo(path).Length; } + private async Task StartupTasksAsync() { await RefreshRepositoryRoleInfoAsyncCommand.Execute(); + await RefreshRepositoryIssuesAsyncCommand.Execute(); + await RefreshWebhooksCommand.Execute(); await DetectLocalChangesAsyncCommand.Execute(); await RendererFileTreeAsync(); SyncCommitsFromRepository(); @@ -336,6 +337,48 @@ public partial class RepositoryViewModel : RoutableViewModelBase if (role >= RepositoryModel.RepositoryRole.Reporter) IsReporterRole = true; if (role >= RepositoryModel.RepositoryRole.Guest) IsGuestRole = true; } + + [ReactiveCommand] + private async Task AddWebhookAsync() + { + if (!IsOwnerRole) return; + + var vm = new WebhookEditDialogViewModel(); + var result = await OverlayDialog.ShowModal( + vm, AppDefaultValues.HostId, UIHelper.DefaultOverlayDialogOptionsYesNo()); + + if (result == DialogResult.Yes) + { + using var l = UIHelper.MakeLoading("Create Webhook..."); + await RepositoryService.C.AddWebhookAsync(Repository, vm.Url, vm.Secret, vm.EventType); + } + } + + [ReactiveCommand] + private async Task DeleteWebhookAsync(Webhook webhook) + { + if (!IsOwnerRole) return; + + var confirm = await UIHelper.SimpleAskAsync($"Do you sure to delete webhook with url at {webhook.TargetUrl} ?"); + if (confirm == DialogResult.Yes) + { + using var l = UIHelper.MakeLoading("Delete webhook..."); + await RepositoryService.C.DeleteWebhookAsync(Repository, webhook.Id); + } + } + + [ReactiveCommand] + public async Task ToggleWebhookAsync(Webhook webhook) + { + if (!IsOwnerRole) return; + + using var l = UIHelper.MakeLoading("Update Webhook..."); + var success = await RepositoryService.C.ToggleWebhookAsync( + Repository, + webhook.Id, + !webhook.IsActive); + } + [ReactiveCommand] private async Task RevertFileTreeToSelectedCommitKeepAsync() @@ -562,6 +605,15 @@ public partial class RepositoryViewModel : RoutableViewModelBase await RepositoryService.C.CloseRepositoryAsync(Repository); await HostScreen.Router.NavigateBack.Execute(); } + + [ReactiveCommand] + private async Task RefreshWebhooksAsync() + { + ShowWebHook = IsDeveloperRole && (Api.C.Status.Value?.AllowWebHook ?? false); + if (!ShowWebHook) return; + using var l = UIHelper.MakeLoading("刷新Webhook列表..."); + await RepositoryService.C.UpdateWebhooksFromServerAsync(Repository); + } [ReactiveCommand] private async ValueTask RefreshRepositoryRoleInfoAsync() diff --git a/Flawless.Client/Views/ModalBox/WebhookEditDialogView.axaml b/Flawless.Client/Views/ModalBox/WebhookEditDialogView.axaml new file mode 100644 index 0000000..e08db7c --- /dev/null +++ b/Flawless.Client/Views/ModalBox/WebhookEditDialogView.axaml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + diff --git a/Flawless.Client/Views/ModalBox/WebhookEditDialogView.axaml.cs b/Flawless.Client/Views/ModalBox/WebhookEditDialogView.axaml.cs new file mode 100644 index 0000000..573047b --- /dev/null +++ b/Flawless.Client/Views/ModalBox/WebhookEditDialogView.axaml.cs @@ -0,0 +1,13 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; + +namespace Flawless.Client.Views.ModalBox; + +public partial class WebhookEditDialogView : UserControl +{ + public WebhookEditDialogView() + { + InitializeComponent(); + } +} \ No newline at end of file diff --git a/Flawless.Client/Views/RepositoryPage/RepoSettingPageView.axaml b/Flawless.Client/Views/RepositoryPage/RepoSettingPageView.axaml index 0e289eb..b9f08f1 100644 --- a/Flawless.Client/Views/RepositoryPage/RepoSettingPageView.axaml +++ b/Flawless.Client/Views/RepositoryPage/RepoSettingPageView.axaml @@ -4,6 +4,7 @@ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:u="https://irihi.tech/ursa" xmlns:vm="using:Flawless.Client.ViewModels" + xmlns:views="clr-namespace:Flawless.Client.Views" x:DataType="vm:RepositoryViewModel" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="Flawless.Client.Views.RepositoryPage.RepoSettingPageView"> @@ -56,8 +57,40 @@