diff --git a/src/Artemis.UI.Shared/Controls/Pagination/Pagination.cs b/src/Artemis.UI.Shared/Controls/Pagination/Pagination.cs
index 26352ee17..35b4fa96e 100644
--- a/src/Artemis.UI.Shared/Controls/Pagination/Pagination.cs
+++ b/src/Artemis.UI.Shared/Controls/Pagination/Pagination.cs
@@ -51,18 +51,20 @@ public partial class Pagination : TemplatedControl
private void OnPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e)
{
- if (e.Property == ValueProperty)
+ if (e.Property == ValueProperty || e.Property == MaximumProperty)
Update();
}
private void NextButtonOnClick(object? sender, RoutedEventArgs e)
{
- Value++;
+ if (Value < Maximum)
+ Value++;
}
private void PreviousButtonOnClick(object? sender, RoutedEventArgs e)
{
- Value--;
+ if (Value > 1)
+ Value--;
}
private void Update()
diff --git a/src/Artemis.UI.Shared/Controls/Pagination/Pagination.properties.cs b/src/Artemis.UI.Shared/Controls/Pagination/Pagination.properties.cs
index 989e13038..65ff27ed4 100644
--- a/src/Artemis.UI.Shared/Controls/Pagination/Pagination.properties.cs
+++ b/src/Artemis.UI.Shared/Controls/Pagination/Pagination.properties.cs
@@ -1,6 +1,7 @@
using System;
using Avalonia;
using Avalonia.Controls.Primitives;
+using Avalonia.Data;
namespace Artemis.UI.Shared.Pagination;
@@ -10,7 +11,7 @@ public partial class Pagination : TemplatedControl
/// Defines the property
///
public static readonly StyledProperty ValueProperty =
- AvaloniaProperty.Register(nameof(Value), 1, enableDataValidation: true, coerce: (p, v) => Math.Clamp(v, 1, ((Pagination) p).Maximum));
+ AvaloniaProperty.Register(nameof(Value), 1, defaultBindingMode: BindingMode.TwoWay);
///
/// Defines the property
diff --git a/src/Artemis.UI.Shared/Routing/Router/Navigation.cs b/src/Artemis.UI.Shared/Routing/Router/Navigation.cs
index ac171100f..567a7b719 100644
--- a/src/Artemis.UI.Shared/Routing/Router/Navigation.cs
+++ b/src/Artemis.UI.Shared/Routing/Router/Navigation.cs
@@ -78,7 +78,8 @@ internal class Navigation
catch (Exception e)
{
Cancel();
- _logger.Error(e, "Failed to navigate to {Path}", resolution.Path);
+ if (e is not TaskCanceledException)
+ _logger.Error(e, "Failed to navigate to {Path}", resolution.Path);
}
}
@@ -96,7 +97,8 @@ internal class Navigation
catch (Exception e)
{
Cancel();
- _logger.Error(e, "Failed to navigate to {Path}", resolution.Path);
+ if (e is not TaskCanceledException)
+ _logger.Error(e, "Failed to navigate to {Path}", resolution.Path);
}
if (CancelIfRequested(args, "OnNavigating", screen))
diff --git a/src/Artemis.UI/Screens/Workshop/Categories/CategoriesViewModel.cs b/src/Artemis.UI/Screens/Workshop/Categories/CategoriesViewModel.cs
index e3b1e0464..a72737141 100644
--- a/src/Artemis.UI/Screens/Workshop/Categories/CategoriesViewModel.cs
+++ b/src/Artemis.UI/Screens/Workshop/Categories/CategoriesViewModel.cs
@@ -1,56 +1,64 @@
using System;
+using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Reactive.Disposables;
-using System.Threading;
-using System.Threading.Tasks;
+using System.Reactive.Linq;
using Artemis.UI.Shared;
using Artemis.WebClient.Workshop;
+using Artemis.WebClient.Workshop.Extensions;
using DynamicData;
+using DynamicData.Binding;
using ReactiveUI;
-using Serilog;
using StrawberryShake;
namespace Artemis.UI.Screens.Workshop.Categories;
public class CategoriesViewModel : ActivatableViewModelBase
{
- private readonly IWorkshopClient _client;
- private readonly ILogger _logger;
- public readonly SourceList _categories;
+ private ObservableAsPropertyHelper?>? _categoryFilters;
- public CategoriesViewModel(ILogger logger, IWorkshopClient client)
+ public CategoriesViewModel(IWorkshopClient client)
{
- _logger = logger;
- _client = client;
- _categories = new SourceList();
- _categories.Connect().Bind(out ReadOnlyObservableCollection categoryViewModels).Subscribe();
-
+ client.GetCategories
+ .Watch(ExecutionStrategy.CacheFirst)
+ .SelectOperationResult(c => c.Categories)
+ .ToObservableChangeSet(c => c.Id)
+ .Transform(c => new CategoryViewModel(c))
+ .Bind(out ReadOnlyObservableCollection categoryViewModels)
+ .Subscribe();
+
Categories = categoryViewModels;
- this.WhenActivated(d => ReactiveCommand.CreateFromTask(GetCategories).Execute().Subscribe().DisposeWith(d));
+
+ this.WhenActivated(d =>
+ {
+ _categoryFilters = Categories.ToObservableChangeSet()
+ .AutoRefresh(c => c.IsSelected)
+ .Filter(e => e.IsSelected)
+ .Select(_ => CreateFilter())
+ .ToProperty(this, vm => vm.CategoryFilters)
+ .DisposeWith(d);
+ });
}
public ReadOnlyObservableCollection Categories { get; }
+ public IReadOnlyList? CategoryFilters => _categoryFilters?.Value;
-
- private async Task GetCategories(CancellationToken cancellationToken)
+ private IReadOnlyList? CreateFilter()
{
- try
- {
- IOperationResult result = await _client.GetCategories.ExecuteAsync(cancellationToken);
- if (result.IsErrorResult())
- _logger.Warning("Failed to retrieve categories {Error}", result.Errors);
+ List categories = Categories.Where(c => c.IsSelected).Select(c => (int?) c.Id).ToList();
+ if (!categories.Any())
+ return null;
- _categories.Edit(l =>
+ List categoryFilters = new();
+ foreach (int? category in categories)
+ {
+ categoryFilters.Add(new EntryFilterInput
{
- l.Clear();
- if (result.Data?.Categories != null)
- l.AddRange(result.Data.Categories.Select(c => new CategoryViewModel(c)));
+ Categories = new ListFilterInputTypeOfCategoryFilterInput {Some = new CategoryFilterInput {Id = new IntOperationFilterInput {Eq = category}}}
});
}
- catch (Exception e)
- {
- _logger.Warning(e, "Failed to retrieve categories");
- }
+
+ return categoryFilters;
}
}
\ No newline at end of file
diff --git a/src/Artemis.UI/Screens/Workshop/Entries/EntryListViewModel.cs b/src/Artemis.UI/Screens/Workshop/Entries/EntryListViewModel.cs
index 7a318fc27..dc393b80c 100644
--- a/src/Artemis.UI/Screens/Workshop/Entries/EntryListViewModel.cs
+++ b/src/Artemis.UI/Screens/Workshop/Entries/EntryListViewModel.cs
@@ -10,13 +10,13 @@ public class EntryListViewModel : ViewModelBase
{
private readonly IRouter _router;
- public EntryListViewModel(IGetEntries_Entries_Nodes entry, IRouter router)
+ public EntryListViewModel(IGetEntries_Entries_Items entry, IRouter router)
{
_router = router;
Entry = entry;
}
- public IGetEntries_Entries_Nodes Entry { get; }
+ public IGetEntries_Entries_Items Entry { get; }
public async Task NavigateToEntry()
{
diff --git a/src/Artemis.UI/Screens/Workshop/Profile/ProfileListView.axaml b/src/Artemis.UI/Screens/Workshop/Profile/ProfileListView.axaml
index 38380c8e7..586bcdd75 100644
--- a/src/Artemis.UI/Screens/Workshop/Profile/ProfileListView.axaml
+++ b/src/Artemis.UI/Screens/Workshop/Profile/ProfileListView.axaml
@@ -3,28 +3,48 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:profile="clr-namespace:Artemis.UI.Screens.Workshop.Profile"
+ xmlns:pagination="clr-namespace:Artemis.UI.Shared.Pagination;assembly=Artemis.UI.Shared"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Artemis.UI.Screens.Workshop.Profile.ProfileListView"
x:DataType="profile:ProfileListViewModel">
-
-
-
- Categories
-
-
-
-
+
+
+
+ Categories
+
+
+
+
-
-
+
+ Filters
+
+
+
+
+
+
+
+
+
+
+
+
-
+
+
+
\ No newline at end of file
diff --git a/src/Artemis.UI/Screens/Workshop/Profile/ProfileListViewModel.cs b/src/Artemis.UI/Screens/Workshop/Profile/ProfileListViewModel.cs
index ecbdb28ff..9ff38f6f0 100644
--- a/src/Artemis.UI/Screens/Workshop/Profile/ProfileListViewModel.cs
+++ b/src/Artemis.UI/Screens/Workshop/Profile/ProfileListViewModel.cs
@@ -1,8 +1,8 @@
using System;
using System.Collections.Generic;
-using System.Collections.ObjectModel;
using System.Linq;
using System.Reactive.Disposables;
+using System.Reactive.Linq;
using System.Threading;
using System.Threading.Tasks;
using Artemis.UI.Screens.Workshop.Categories;
@@ -11,8 +11,6 @@ using Artemis.UI.Screens.Workshop.Parameters;
using Artemis.UI.Shared;
using Artemis.UI.Shared.Routing;
using Artemis.WebClient.Workshop;
-using DynamicData;
-using DynamicData.Alias;
using ReactiveUI;
using StrawberryShake;
@@ -20,35 +18,44 @@ namespace Artemis.UI.Screens.Workshop.Profile;
public class ProfileListViewModel : RoutableScreen, IWorkshopViewModel
{
- private readonly SourceList _entries;
+ private readonly IRouter _router;
private readonly IWorkshopClient _workshopClient;
+ private readonly ObservableAsPropertyHelper _showPagination;
+ private List? _entries;
private int _page;
+ private int _totalPages = 1;
+ private int _entriesPerPage = 5;
public ProfileListViewModel(IWorkshopClient workshopClient, IRouter router, CategoriesViewModel categoriesViewModel)
{
_workshopClient = workshopClient;
+ _router = router;
+ _showPagination = this.WhenAnyValue(vm => vm.TotalPages).Select(t => t > 1).ToProperty(this, vm => vm.ShowPagination);
+
CategoriesViewModel = categoriesViewModel;
- _entries = new SourceList();
- _entries.Connect()
- .Transform(e => new EntryListViewModel(e, router))
- .Bind(out ReadOnlyObservableCollection observableEntries)
- .Subscribe();
-
- this.WhenActivated(d =>
+ // Respond to page changes
+ this.WhenAnyValue(vm => vm.Page).Skip(1).Subscribe(p => Task.Run(() => _router.Navigate($"workshop/profiles/{p}")));
+ // Respond to filter changes
+ this.WhenActivated(d => CategoriesViewModel.WhenAnyValue(vm => vm.CategoryFilters).Skip(1).Subscribe(_ =>
{
- CategoriesViewModel._categories.Connect()
- .AutoRefresh(c => c.IsSelected)
- .Filter(e => e.IsSelected)
- .Select(e => e.Id)
- .Subscribe(_ => ReactiveCommand.CreateFromTask(GetEntries).Execute().Subscribe())
- .DisposeWith(d);
- });
- Entries = observableEntries;
+ // Reset to page one, will trigger a query
+ if (Page != 1)
+ Page = 1;
+ // If already at page one, force a query
+ else
+ Task.Run(() => Query(CancellationToken.None));
+ }).DisposeWith(d));
}
+ public bool ShowPagination => _showPagination.Value;
public CategoriesViewModel CategoriesViewModel { get; }
- public ReadOnlyObservableCollection Entries { get; set; }
+
+ public List? Entries
+ {
+ get => _entries;
+ set => RaiseAndSetIfChanged(ref _entries, value);
+ }
public int Page
{
@@ -56,32 +63,49 @@ public class ProfileListViewModel : RoutableScreen RaiseAndSetIfChanged(ref _page, value);
}
+ public int TotalPages
+ {
+ get => _totalPages;
+ set => RaiseAndSetIfChanged(ref _totalPages, value);
+ }
+
+ public int EntriesPerPage
+ {
+ get => _entriesPerPage;
+ set => RaiseAndSetIfChanged(ref _entriesPerPage, value);
+ }
+
public override async Task OnNavigating(WorkshopListParameters parameters, NavigationArguments args, CancellationToken cancellationToken)
{
Page = Math.Max(1, parameters.Page);
- await GetEntries(cancellationToken);
+
+ // Throttle page changes
+ await Task.Delay(200, cancellationToken);
+
+ if (!cancellationToken.IsCancellationRequested)
+ await Query(cancellationToken);
}
- private async Task GetEntries(CancellationToken cancellationToken)
+ private async Task Query(CancellationToken cancellationToken)
{
- IOperationResult result = await _workshopClient.GetEntries.ExecuteAsync(CreateFilter(), cancellationToken);
- if (result.IsErrorResult() || result.Data?.Entries?.Nodes == null)
- return;
-
- _entries.Edit(e =>
+ EntryFilterInput filter = GetFilter();
+ IOperationResult entries = await _workshopClient.GetEntries.ExecuteAsync(filter, EntriesPerPage * (Page - 1), EntriesPerPage, cancellationToken);
+ if (!entries.IsErrorResult() && entries.Data?.Entries?.Items != null)
{
- e.Clear();
- e.AddRange(result.Data.Entries.Nodes);
- });
+ Entries = entries.Data.Entries.Items.Select(n => new EntryListViewModel(n, _router)).ToList();
+ TotalPages = (int) Math.Ceiling(entries.Data.Entries.TotalCount / (double) EntriesPerPage);
+ }
+ else
+ TotalPages = 1;
}
- private EntryFilterInput CreateFilter()
+ private EntryFilterInput GetFilter()
{
- EntryFilterInput filter = new() {EntryType = new EntryTypeOperationFilterInput {Eq = WebClient.Workshop.EntryType.Profile}};
-
- List categories = CategoriesViewModel.Categories.Where(c => c.IsSelected).Select(c => (int?) c.Id).ToList();
- if (categories.Any())
- filter.Categories = new ListFilterInputTypeOfCategoryFilterInput {All = new CategoryFilterInput {Id = new IntOperationFilterInput {In = categories}}};
+ EntryFilterInput filter = new()
+ {
+ EntryType = new EntryTypeOperationFilterInput {Eq = WebClient.Workshop.EntryType.Profile},
+ And = CategoriesViewModel.CategoryFilters
+ };
return filter;
}
diff --git a/src/Artemis.WebClient.Workshop/Artemis.WebClient.Workshop.csproj b/src/Artemis.WebClient.Workshop/Artemis.WebClient.Workshop.csproj
index 1ba5f26ae..e09fefe42 100644
--- a/src/Artemis.WebClient.Workshop/Artemis.WebClient.Workshop.csproj
+++ b/src/Artemis.WebClient.Workshop/Artemis.WebClient.Workshop.csproj
@@ -13,8 +13,10 @@
+
+
diff --git a/src/Artemis.WebClient.Workshop/Extensions/ReactiveExtensions.cs b/src/Artemis.WebClient.Workshop/Extensions/ReactiveExtensions.cs
new file mode 100644
index 000000000..266574c01
--- /dev/null
+++ b/src/Artemis.WebClient.Workshop/Extensions/ReactiveExtensions.cs
@@ -0,0 +1,30 @@
+using System.Reactive.Linq;
+using ReactiveUI;
+using StrawberryShake;
+
+namespace Artemis.WebClient.Workshop.Extensions;
+
+public static class ReactiveExtensions
+{
+ ///
+ /// Projects the data of the provided operation result into a new observable sequence if the result is successfull and
+ /// contains data.
+ ///
+ /// A sequence of operation results to invoke a transform function on.
+ /// A transform function to apply to the data of each source element.
+ /// The type of data contained in the operation result.
+ /// The type of data to project from the result.
+ ///
+ /// An observable sequence whose elements are the result of invoking the transform function on each element of
+ /// source.
+ ///
+ public static IObservable SelectOperationResult(this IObservable> source, Func selector) where TSource : class
+ {
+ return source
+ .Where(s => !s.Errors.Any())
+ .Select(s => s.Data)
+ .WhereNotNull()
+ .Select(selector)
+ .WhereNotNull();
+ }
+}
\ No newline at end of file
diff --git a/src/Artemis.WebClient.Workshop/Queries/GetEntries.graphql b/src/Artemis.WebClient.Workshop/Queries/GetEntries.graphql
index 3ae3fc34b..c7e639c31 100644
--- a/src/Artemis.WebClient.Workshop/Queries/GetEntries.graphql
+++ b/src/Artemis.WebClient.Workshop/Queries/GetEntries.graphql
@@ -1,6 +1,7 @@
-query GetEntries($filter: EntryFilterInput) {
- entries(where: $filter) {
- nodes {
+query GetEntries($filter: EntryFilterInput $skip: Int $take: Int) {
+ entries(where: $filter skip: $skip take: $take) {
+ totalCount
+ items {
id
author
name
diff --git a/src/Artemis.WebClient.Workshop/schema.graphql b/src/Artemis.WebClient.Workshop/schema.graphql
index c41fd39ba..59efce290 100644
--- a/src/Artemis.WebClient.Workshop/schema.graphql
+++ b/src/Artemis.WebClient.Workshop/schema.graphql
@@ -11,24 +11,21 @@ type Category {
name: String!
}
-"A connection to a list of items."
-type EntriesConnection {
- "A list of edges."
- edges: [EntriesEdge!]
- "A flattened list of the nodes."
- nodes: [Entry!]
- "Information to aid in pagination."
- pageInfo: PageInfo!
- "Identifies the total count of items in the connection."
- totalCount: Int!
+"Information about the offset pagination."
+type CollectionSegmentInfo {
+ "Indicates whether more items exist following the set defined by the clients arguments."
+ hasNextPage: Boolean!
+ "Indicates whether more items exist prior the set defined by the clients arguments."
+ hasPreviousPage: Boolean!
}
-"An edge in a connection."
-type EntriesEdge {
- "A cursor for use in pagination."
- cursor: String!
- "The item at the end of the edge."
- node: Entry!
+"A segment of a collection."
+type EntriesCollectionSegment {
+ "A flattened list of the items."
+ items: [Entry!]
+ "Information to aid in pagination."
+ pageInfo: CollectionSegmentInfo!
+ totalCount: Int!
}
type Entry {
@@ -54,35 +51,13 @@ type Image {
}
type Mutation {
- addEntry(input: EntryInput!): Entry
-}
-
-"Information about pagination in a connection."
-type PageInfo {
- "When paginating forwards, the cursor to continue."
- endCursor: String
- "Indicates whether more edges exist following the set defined by the clients arguments."
- hasNextPage: Boolean!
- "Indicates whether more edges exist prior the set defined by the clients arguments."
- hasPreviousPage: Boolean!
- "When paginating backwards, the cursor to continue."
- startCursor: String
+ addEntry(input: CreateEntryInput!): Entry
+ updateEntry(input: UpdateEntryInput!): Entry
}
type Query {
categories(order: [CategorySortInput!], where: CategoryFilterInput): [Category!]!
- entries(
- "Returns the elements in the list that come after the specified cursor."
- after: String,
- "Returns the elements in the list that come before the specified cursor."
- before: String,
- "Returns the first _n_ elements from the list."
- first: Int,
- "Returns the last _n_ elements from the list."
- last: Int,
- order: [EntrySortInput!],
- where: EntryFilterInput
- ): EntriesConnection
+ entries(order: [EntrySortInput!], skip: Int, take: Int, where: EntryFilterInput): EntriesCollectionSegment
entry(id: UUID!): Entry
searchEntries(input: String!, order: [EntrySortInput!], type: EntryType, where: EntryFilterInput): [Entry!]!
}
@@ -142,6 +117,15 @@ input CategorySortInput {
name: SortEnumType
}
+input CreateEntryInput {
+ categories: [Int!]!
+ description: String!
+ entryType: EntryType!
+ name: String!
+ summary: String!
+ tags: [String!]!
+}
+
input DateTimeOperationFilterInput {
eq: DateTime
gt: DateTime
@@ -176,13 +160,6 @@ input EntryFilterInput {
tags: ListFilterInputTypeOfTagFilterInput
}
-input EntryInput {
- description: String!
- entryType: EntryType!
- name: String!
- tags: [String!]!
-}
-
input EntrySortInput {
author: SortEnumType
authorId: SortEnumType
@@ -308,6 +285,15 @@ input TagFilterInput {
or: [TagFilterInput!]
}
+input UpdateEntryInput {
+ categories: [Int!]!
+ description: String!
+ id: UUID!
+ name: String!
+ summary: String!
+ tags: [String!]!
+}
+
input UuidOperationFilterInput {
eq: UUID
gt: UUID