From 3caf782d8ed35f757fe73856b20294e33bcfe38d Mon Sep 17 00:00:00 2001 From: Robert Date: Sat, 19 Apr 2025 11:37:28 +0200 Subject: [PATCH] Workshop - Add ability to mark entry as default (admin only) Workshop - Allow editing other user's entries (admin only) Workshop - Show last update date in entry list instead of creation date --- .../Entries/Details/EntryInfoView.axaml | 29 ++++++++++++------- .../Entries/Details/EntryInfoViewModel.cs | 16 ++++++++-- .../Details/EntrySpecificationsView.axaml | 2 ++ .../Details/EntrySpecificationsViewModel.cs | 10 +++++-- .../Entries/List/EntryListItemView.axaml | 2 +- .../Library/SubmissionDetailsViewModel.cs | 7 +++-- .../Library/Tabs/SubmissionsTabItemView.axaml | 2 +- .../Models/SubmissionWizardState.cs | 1 + .../Steps/ChangelogStepViewModel.cs | 7 +++-- .../Steps/SpecificationsStepViewModel.cs | 2 ++ .../Queries/Fragments.graphql | 7 +++++ .../Services/AuthenticationService.cs | 6 ++++ .../Interfaces/IAuthenticationService.cs | 1 + 13 files changed, 69 insertions(+), 23 deletions(-) diff --git a/src/Artemis.UI/Screens/Workshop/Entries/Details/EntryInfoView.axaml b/src/Artemis.UI/Screens/Workshop/Entries/Details/EntryInfoView.axaml index 94b2e1688..449639c4e 100644 --- a/src/Artemis.UI/Screens/Workshop/Entries/Details/EntryInfoView.axaml +++ b/src/Artemis.UI/Screens/Workshop/Entries/Details/EntryInfoView.axaml @@ -15,7 +15,7 @@ - - + + + + + - + diff --git a/src/Artemis.UI/Screens/Workshop/Entries/Details/EntryInfoViewModel.cs b/src/Artemis.UI/Screens/Workshop/Entries/Details/EntryInfoViewModel.cs index 6cf02efb1..6700f0bd3 100644 --- a/src/Artemis.UI/Screens/Workshop/Entries/Details/EntryInfoViewModel.cs +++ b/src/Artemis.UI/Screens/Workshop/Entries/Details/EntryInfoViewModel.cs @@ -25,7 +25,7 @@ public partial class EntryInfoViewModel : ActivatableViewModelBase [Notify] private DateTimeOffset? _updatedAt; [Notify] private bool _canBeManaged; - public EntryInfoViewModel(IRouter router, INotificationService notificationService, IWorkshopService workshopService) + public EntryInfoViewModel(IRouter router, INotificationService notificationService, IWorkshopService workshopService, IAuthenticationService authenticationService) { _router = router; _notificationService = notificationService; @@ -38,8 +38,12 @@ public partial class EntryInfoViewModel : ActivatableViewModelBase .Subscribe(_ => CanBeManaged = Entry != null && Entry.EntryType != EntryType.Profile && workshopService.GetInstalledEntry(Entry.Id) != null) .DisposeWith(d); }); + + IsAdministrator = authenticationService.GetRoles().Contains("Administrator"); } - + + public bool IsAdministrator { get; } + public void SetEntry(IEntryDetails? entry) { Entry = entry; @@ -55,6 +59,14 @@ public partial class EntryInfoViewModel : ActivatableViewModelBase await Shared.UI.Clipboard.SetTextAsync($"{WorkshopConstants.WORKSHOP_URL}/entries/{Entry.Id}/{StringUtilities.UrlFriendly(Entry.Name)}"); _notificationService.CreateNotification().WithTitle("Copied share link to clipboard.").Show(); } + + public async Task GoToEdit() + { + if (Entry == null) + return; + + await _router.Navigate($"workshop/library/submissions/{Entry.Id}"); + } public async Task GoToManage() { diff --git a/src/Artemis.UI/Screens/Workshop/Entries/Details/EntrySpecificationsView.axaml b/src/Artemis.UI/Screens/Workshop/Entries/Details/EntrySpecificationsView.axaml index 16a0b4b5a..dda4ba091 100644 --- a/src/Artemis.UI/Screens/Workshop/Entries/Details/EntrySpecificationsView.axaml +++ b/src/Artemis.UI/Screens/Workshop/Entries/Details/EntrySpecificationsView.axaml @@ -63,6 +63,8 @@ + + Download by default (admin only) diff --git a/src/Artemis.UI/Screens/Workshop/Entries/Details/EntrySpecificationsViewModel.cs b/src/Artemis.UI/Screens/Workshop/Entries/Details/EntrySpecificationsViewModel.cs index 041af76cc..bea16889d 100644 --- a/src/Artemis.UI/Screens/Workshop/Entries/Details/EntrySpecificationsViewModel.cs +++ b/src/Artemis.UI/Screens/Workshop/Entries/Details/EntrySpecificationsViewModel.cs @@ -11,6 +11,7 @@ using Artemis.UI.Screens.Workshop.Categories; using Artemis.UI.Shared; using Artemis.UI.Shared.Services; using Artemis.WebClient.Workshop; +using Artemis.WebClient.Workshop.Services; using Avalonia.Media.Imaging; using AvaloniaEdit.Document; using DynamicData; @@ -34,10 +35,11 @@ public partial class EntrySpecificationsViewModel : ValidatableViewModelBase [Notify] private string _name = string.Empty; [Notify] private string _summary = string.Empty; [Notify] private string _description = string.Empty; + [Notify] private bool _isDefault; [Notify] private Bitmap? _iconBitmap; [Notify(Setter.Private)] private bool _iconChanged; - public EntrySpecificationsViewModel(IWorkshopClient workshopClient, IWindowService windowService) + public EntrySpecificationsViewModel(IWorkshopClient workshopClient, IWindowService windowService, IAuthenticationService authenticationService) { _workshopClient = workshopClient; _windowService = windowService; @@ -65,8 +67,9 @@ public partial class EntrySpecificationsViewModel : ValidatableViewModelBase _descriptionValid = descriptionRule.ValidationChanged.Select(c => c.IsValid).ToProperty(this, vm => vm.DescriptionValid); this.WhenActivatedAsync(async _ => await PopulateCategories()); + IsAdministrator = authenticationService.GetRoles().Contains("Administrator"); } - + public ReactiveCommand SelectIcon { get; } public ObservableCollection Categories { get; } = new(); @@ -76,7 +79,8 @@ public partial class EntrySpecificationsViewModel : ValidatableViewModelBase public bool CategoriesValid => _categoriesValid.Value ; public bool IconValid => _iconValid.Value; public bool DescriptionValid => _descriptionValid.Value; - + public bool IsAdministrator { get; } + public List PreselectedCategories { get; set; } = new(); private async Task ExecuteSelectIcon() diff --git a/src/Artemis.UI/Screens/Workshop/Entries/List/EntryListItemView.axaml b/src/Artemis.UI/Screens/Workshop/Entries/List/EntryListItemView.axaml index cae7a1934..af26a3da1 100644 --- a/src/Artemis.UI/Screens/Workshop/Entries/List/EntryListItemView.axaml +++ b/src/Artemis.UI/Screens/Workshop/Entries/List/EntryListItemView.axaml @@ -79,7 +79,7 @@ - + diff --git a/src/Artemis.UI/Screens/Workshop/Library/SubmissionDetailsViewModel.cs b/src/Artemis.UI/Screens/Workshop/Library/SubmissionDetailsViewModel.cs index 7829b7261..da3216050 100644 --- a/src/Artemis.UI/Screens/Workshop/Library/SubmissionDetailsViewModel.cs +++ b/src/Artemis.UI/Screens/Workshop/Library/SubmissionDetailsViewModel.cs @@ -59,7 +59,7 @@ public partial class SubmissionDetailsViewModel : RoutableScreen DiscardChanges = ReactiveCommand.CreateFromTask(ExecuteDiscardChanges, this.WhenAnyValue(vm => vm.HasChanges)); SaveChanges = ReactiveCommand.CreateFromTask(ExecuteSaveChanges, this.WhenAnyValue(vm => vm.HasChanges)); } - + public ObservableCollection Images { get; } = new(); public ReactiveCommand AddImage { get; } public ReactiveCommand SaveChanges { get; } @@ -105,6 +105,7 @@ public partial class SubmissionDetailsViewModel : RoutableScreen specificationsViewModel.Name = Entry.Name; specificationsViewModel.Summary = Entry.Summary; specificationsViewModel.Description = Entry.Description; + specificationsViewModel.IsDefault = Entry.IsDefault; specificationsViewModel.PreselectedCategories = Entry.Categories.Select(c => c.Id).ToList(); specificationsViewModel.Tags.Clear(); @@ -169,6 +170,7 @@ public partial class SubmissionDetailsViewModel : RoutableScreen HasChanges = EntrySpecificationsViewModel.Name != Entry.Name || EntrySpecificationsViewModel.Description != Entry.Description || EntrySpecificationsViewModel.Summary != Entry.Summary || + EntrySpecificationsViewModel.IsDefault != Entry.IsDefault || EntrySpecificationsViewModel.IconChanged || !tags.SequenceEqual(Entry.Tags.Select(t => t.Name).OrderBy(t => t)) || !categories.SequenceEqual(Entry.Categories.Select(c => c.Id).OrderBy(c => c)) || @@ -192,6 +194,7 @@ public partial class SubmissionDetailsViewModel : RoutableScreen Name = EntrySpecificationsViewModel.Name, Summary = EntrySpecificationsViewModel.Summary, Description = EntrySpecificationsViewModel.Description, + IsDefault = EntrySpecificationsViewModel.IsDefault, Categories = EntrySpecificationsViewModel.SelectedCategories, Tags = EntrySpecificationsViewModel.Tags }; @@ -233,7 +236,7 @@ public partial class SubmissionDetailsViewModel : RoutableScreen HasChanges = false; await _router.Reload(); } - + private async Task ExecuteAddImage(CancellationToken arg) { string[]? result = await _windowService.CreateOpenFileDialog().WithAllowMultiple().HavingFilter(f => f.WithBitmaps()).ShowAsync(); diff --git a/src/Artemis.UI/Screens/Workshop/Library/Tabs/SubmissionsTabItemView.axaml b/src/Artemis.UI/Screens/Workshop/Library/Tabs/SubmissionsTabItemView.axaml index b45a8d3e8..822f24c71 100644 --- a/src/Artemis.UI/Screens/Workshop/Library/Tabs/SubmissionsTabItemView.axaml +++ b/src/Artemis.UI/Screens/Workshop/Library/Tabs/SubmissionsTabItemView.axaml @@ -67,7 +67,7 @@ - + diff --git a/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Models/SubmissionWizardState.cs b/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Models/SubmissionWizardState.cs index aaf688ef6..5e6927690 100644 --- a/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Models/SubmissionWizardState.cs +++ b/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Models/SubmissionWizardState.cs @@ -32,6 +32,7 @@ public class SubmissionWizardState : IDisposable public Stream? Icon { get; set; } public string Summary { get; set; } = string.Empty; public string Description { get; set; } = string.Empty; + public bool IsDefault { get; set; } public List Categories { get; set; } = new(); public List Tags { get; set; } = new(); diff --git a/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/ChangelogStepViewModel.cs b/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/ChangelogStepViewModel.cs index e2e97795f..e84d4a69a 100644 --- a/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/ChangelogStepViewModel.cs +++ b/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/ChangelogStepViewModel.cs @@ -1,3 +1,4 @@ +using System.ComponentModel.DataAnnotations; using System.Reactive.Disposables; using Artemis.UI.Screens.Workshop.SubmissionWizard.Steps.Layout; using Artemis.UI.Screens.Workshop.SubmissionWizard.Steps.Plugin; @@ -10,7 +11,7 @@ namespace Artemis.UI.Screens.Workshop.SubmissionWizard.Steps; public partial class ChangelogStepViewModel : SubmissionViewModel { - [Notify] private string? _changelog; + [Notify] private string _changelog = string.Empty; public ChangelogStepViewModel() { @@ -18,7 +19,7 @@ public partial class ChangelogStepViewModel : SubmissionViewModel Continue = ReactiveCommand.Create(ExecuteContinue); ContinueText = "Submit"; - this.WhenActivated((CompositeDisposable _) => Changelog = State.Changelog); + this.WhenActivated((CompositeDisposable _) => Changelog = State.Changelog ?? string.Empty); } private void ExecuteContinue() @@ -29,7 +30,7 @@ public partial class ChangelogStepViewModel : SubmissionViewModel private void ExecuteGoBack() { - State.Changelog = Changelog; + State.Changelog = string.IsNullOrWhiteSpace(Changelog) ? null : Changelog; if (State.EntryType == EntryType.Layout) State.ChangeScreen(); else if (State.EntryType == EntryType.Plugin) diff --git a/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/SpecificationsStepViewModel.cs b/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/SpecificationsStepViewModel.cs index 678aea918..01a5c19fc 100644 --- a/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/SpecificationsStepViewModel.cs +++ b/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/SpecificationsStepViewModel.cs @@ -65,6 +65,7 @@ public partial class SpecificationsStepViewModel : SubmissionViewModel viewModel.Name = State.Name; viewModel.Summary = State.Summary; viewModel.Description = State.Description; + viewModel.IsDefault = State.IsDefault; // Tags viewModel.Tags.Clear(); @@ -93,6 +94,7 @@ public partial class SpecificationsStepViewModel : SubmissionViewModel State.Name = EntrySpecificationsViewModel.Name; State.Summary = EntrySpecificationsViewModel.Summary; State.Description = EntrySpecificationsViewModel.Description; + State.IsDefault = EntrySpecificationsViewModel.IsDefault; // Categories and tasks State.Categories = EntrySpecificationsViewModel.Categories.Where(c => c.IsSelected).Select(c => c.Id).ToList(); diff --git a/src/Artemis.WebClient.Workshop/Queries/Fragments.graphql b/src/Artemis.WebClient.Workshop/Queries/Fragments.graphql index cf0c3c779..a1f8eb669 100644 --- a/src/Artemis.WebClient.Workshop/Queries/Fragments.graphql +++ b/src/Artemis.WebClient.Workshop/Queries/Fragments.graphql @@ -26,6 +26,10 @@ fragment submittedEntry on Entry { entryType downloads createdAt + isDefault + latestRelease { + ...release + } } fragment entrySummary on Entry { @@ -38,6 +42,9 @@ fragment entrySummary on Entry { downloads createdAt latestReleaseId + latestRelease { + ...release + } categories { ...category } diff --git a/src/Artemis.WebClient.Workshop/Services/AuthenticationService.cs b/src/Artemis.WebClient.Workshop/Services/AuthenticationService.cs index 9c0dc6d7b..7e7e76f47 100644 --- a/src/Artemis.WebClient.Workshop/Services/AuthenticationService.cs +++ b/src/Artemis.WebClient.Workshop/Services/AuthenticationService.cs @@ -283,6 +283,12 @@ internal class AuthenticationService : CorePropertyChanged, IAuthenticationServi return emailVerified?.Value.ToLower() == "true"; } + /// + public List GetRoles() + { + return Claims.Where(c => c.Type == JwtClaimTypes.Role).Select(c => c.Value).ToList(); + } + private async Task InternalAutoLogin(bool force = false) { if (!force && _isLoggedInSubject.Value) diff --git a/src/Artemis.WebClient.Workshop/Services/Interfaces/IAuthenticationService.cs b/src/Artemis.WebClient.Workshop/Services/Interfaces/IAuthenticationService.cs index 5d8f908b3..66e1392be 100644 --- a/src/Artemis.WebClient.Workshop/Services/Interfaces/IAuthenticationService.cs +++ b/src/Artemis.WebClient.Workshop/Services/Interfaces/IAuthenticationService.cs @@ -15,4 +15,5 @@ public interface IAuthenticationService : IProtectedArtemisService Task Login(CancellationToken cancellationToken); Task Logout(); bool GetIsEmailVerified(); + List GetRoles(); } \ No newline at end of file