From f0cbc3e56196828b6e3022a5ac609a552b5cfd3a Mon Sep 17 00:00:00 2001 From: Robert Date: Sat, 6 Dec 2025 20:27:48 +0100 Subject: [PATCH] Workshop - Add button to upload submission directly from library Workshop - While creating submission allow fitting the icon instead of cropping Workshop - Add the ability to filter default entries --- src/Artemis.UI/Extensions/Bitmap.cs | 71 ++++++--- .../ProfileConfigurationEditViewModel.cs | 2 +- .../Details/EntrySpecificationsView.axaml | 2 +- .../Details/EntrySpecificationsViewModel.cs | 29 +++- .../Workshop/Entries/List/EntryListView.axaml | 17 +- .../Entries/List/EntryListViewModel.cs | 7 +- .../Library/Tabs/SubmissionsTabView.axaml | 1 + .../Steps/SpecificationsStepViewModel.cs | 2 +- .../WorkshopConstants.cs | 8 +- .../graphql.config.yml | 2 +- src/Artemis.WebClient.Workshop/schema.graphql | 146 +++++++++++++++--- 11 files changed, 227 insertions(+), 60 deletions(-) diff --git a/src/Artemis.UI/Extensions/Bitmap.cs b/src/Artemis.UI/Extensions/Bitmap.cs index e30bc754f..13a07c35b 100644 --- a/src/Artemis.UI/Extensions/Bitmap.cs +++ b/src/Artemis.UI/Extensions/Bitmap.cs @@ -1,6 +1,5 @@ using System; using System.IO; -using System.Threading.Tasks; using Avalonia.Media.Imaging; using SkiaSharp; @@ -8,41 +7,75 @@ namespace Artemis.UI.Extensions; public class BitmapExtensions { - public static Bitmap LoadAndResize(string file, int size) + public static Bitmap LoadAndResize(string file, int size, bool fit) { using SKBitmap source = SKBitmap.Decode(file); - return Resize(source, size); + return Resize(source, size, fit); } - public static Bitmap LoadAndResize(Stream stream, int size) + public static Bitmap LoadAndResize(Stream stream, int size, bool fit) { stream.Seek(0, SeekOrigin.Begin); using MemoryStream copy = new(); stream.CopyTo(copy); copy.Seek(0, SeekOrigin.Begin); using SKBitmap source = SKBitmap.Decode(copy); - return Resize(source, size); + return Resize(source, size, fit); } - private static Bitmap Resize(SKBitmap source, int size) + private static Bitmap Resize(SKBitmap source, int size, bool fit) { - // Get smaller dimension. - int minDim = Math.Min(source.Width, source.Height); + if (!fit) + { + // Get smaller dimension. + int minDim = Math.Min(source.Width, source.Height); - // Calculate crop rectangle position for center crop. - int deltaX = (source.Width - minDim) / 2; - int deltaY = (source.Height - minDim) / 2; + // Calculate crop rectangle position for center crop. + int deltaX = (source.Width - minDim) / 2; + int deltaY = (source.Height - minDim) / 2; - // Create crop rectangle. - SKRectI rect = new(deltaX, deltaY, deltaX + minDim, deltaY + minDim); + // Create crop rectangle. + SKRectI rect = new(deltaX, deltaY, deltaX + minDim, deltaY + minDim); - // Do the actual cropping of the bitmap. - using SKBitmap croppedBitmap = new(minDim, minDim); - source.ExtractSubset(croppedBitmap, rect); + // Do the actual cropping of the bitmap. + using SKBitmap croppedBitmap = new(minDim, minDim); + source.ExtractSubset(croppedBitmap, rect); - // Resize to the desired size after cropping. - using SKBitmap resizedBitmap = croppedBitmap.Resize(new SKImageInfo(size, size), SKFilterQuality.High); + // Resize to the desired size after cropping. + using SKBitmap resizedBitmap = croppedBitmap.Resize(new SKImageInfo(size, size), SKFilterQuality.High); - return new Bitmap(resizedBitmap.Encode(SKEncodedImageFormat.Png, 100).AsStream()); + // Encode via SKImage for compatibility + using SKImage image = SKImage.FromBitmap(resizedBitmap); + using SKData data = image.Encode(SKEncodedImageFormat.Png, 100); + return new Bitmap(data.AsStream()); + } + else + { + // Fit the image inside a size x size square without cropping. + // Compute scale based on the larger dimension. + float scale = (float)size / Math.Max(source.Width, source.Height); + int targetW = Math.Max(1, (int)Math.Floor(source.Width * scale)); + int targetH = Math.Max(1, (int)Math.Floor(source.Height * scale)); + + // Resize maintaining aspect ratio. + using SKBitmap resizedAspect = source.Resize(new SKImageInfo(targetW, targetH), SKFilterQuality.High); + + // Create final square canvas and draw the fitted image centered. + using SKBitmap finalBitmap = new(size, size); + using (SKCanvas canvas = new(finalBitmap)) + { + // Clear to transparent. + canvas.Clear(SKColors.Transparent); + + int offsetX = (size - targetW) / 2; + int offsetY = (size - targetH) / 2; + canvas.DrawBitmap(resizedAspect, new SKPoint(offsetX, offsetY)); + canvas.Flush(); + } + + using SKImage image = SKImage.FromBitmap(finalBitmap); + using SKData data = image.Encode(SKEncodedImageFormat.Png, 100); + return new Bitmap(data.AsStream()); + } } } \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Sidebar/Dialogs/ProfileConfigurationEditViewModel.cs b/src/Artemis.UI/Screens/Sidebar/Dialogs/ProfileConfigurationEditViewModel.cs index afa278f98..7518b2862 100644 --- a/src/Artemis.UI/Screens/Sidebar/Dialogs/ProfileConfigurationEditViewModel.cs +++ b/src/Artemis.UI/Screens/Sidebar/Dialogs/ProfileConfigurationEditViewModel.cs @@ -187,7 +187,7 @@ public partial class ProfileConfigurationEditViewModel : DialogViewModelBase - Icon required + Shrink diff --git a/src/Artemis.UI/Screens/Workshop/Entries/Details/EntrySpecificationsViewModel.cs b/src/Artemis.UI/Screens/Workshop/Entries/Details/EntrySpecificationsViewModel.cs index 4ea3ed070..d2cdd265f 100644 --- a/src/Artemis.UI/Screens/Workshop/Entries/Details/EntrySpecificationsViewModel.cs +++ b/src/Artemis.UI/Screens/Workshop/Entries/Details/EntrySpecificationsViewModel.cs @@ -38,15 +38,18 @@ public partial class EntrySpecificationsViewModel : ValidatableViewModelBase [Notify] private bool _isDefault; [Notify] private bool _isEssential; [Notify] private bool _isDeviceProvider; + [Notify] private bool _fit; [Notify] private Bitmap? _iconBitmap; [Notify(Setter.Private)] private bool _iconChanged; + private string? _lastIconPath; + public EntrySpecificationsViewModel(IWorkshopClient workshopClient, IWindowService windowService, IAuthenticationService authenticationService) { _workshopClient = workshopClient; _windowService = windowService; SelectIcon = ReactiveCommand.CreateFromTask(ExecuteSelectIcon); - + Categories.ToObservableChangeSet() .AutoRefresh(c => c.IsSelected) .Filter(c => c.IsSelected) @@ -68,23 +71,24 @@ public partial class EntrySpecificationsViewModel : ValidatableViewModelBase _categoriesValid = categoriesRule.ValidationChanged.Select(c => c.IsValid).ToProperty(this, vm => vm.CategoriesValid); _descriptionValid = descriptionRule.ValidationChanged.Select(c => c.IsValid).ToProperty(this, vm => vm.DescriptionValid); - this.WhenActivatedAsync(async _ => await PopulateCategories()); IsAdministrator = authenticationService.GetRoles().Contains("Administrator"); + this.WhenActivatedAsync(async _ => await PopulateCategories()); + this.WhenAnyValue(vm => vm.Fit).Subscribe(_ => UpdateIcon()); } - + public ReactiveCommand SelectIcon { get; } public ObservableCollection Categories { get; } = new(); public ObservableCollection Tags { get; } = new(); public ReadOnlyObservableCollection SelectedCategories { get; } - - public bool CategoriesValid => _categoriesValid.Value ; + + 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() { string[]? result = await _windowService.CreateOpenFileDialog() @@ -94,8 +98,17 @@ public partial class EntrySpecificationsViewModel : ValidatableViewModelBase if (result == null) return; + _lastIconPath = result[0]; + UpdateIcon(); + } + + private void UpdateIcon() + { + if (_lastIconPath == null) + return; + IconBitmap?.Dispose(); - IconBitmap = BitmapExtensions.LoadAndResize(result[0], 128); + IconBitmap = BitmapExtensions.LoadAndResize(_lastIconPath, 128, Fit); IconChanged = true; } diff --git a/src/Artemis.UI/Screens/Workshop/Entries/List/EntryListView.axaml b/src/Artemis.UI/Screens/Workshop/Entries/List/EntryListView.axaml index 8dbdd04b5..fdbebe7ca 100644 --- a/src/Artemis.UI/Screens/Workshop/Entries/List/EntryListView.axaml +++ b/src/Artemis.UI/Screens/Workshop/Entries/List/EntryListView.axaml @@ -3,6 +3,7 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:list="clr-namespace:Artemis.UI.Screens.Workshop.Entries.List" + xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="Artemis.UI.Screens.Workshop.Entries.List.EntryListView" x:DataType="list:EntryListViewModel"> @@ -26,6 +27,20 @@ HorizontalAlignment="Right" Width="300" IsVisible="{CompiledBinding ShowCategoryFilter}"> + + + Categories + + + + + + Default entries + + + + + Categories @@ -59,7 +74,7 @@ Modify or clear your filters to view other entries - + diff --git a/src/Artemis.UI/Screens/Workshop/Entries/List/EntryListViewModel.cs b/src/Artemis.UI/Screens/Workshop/Entries/List/EntryListViewModel.cs index a6b5309c7..cf7f7f816 100644 --- a/src/Artemis.UI/Screens/Workshop/Entries/List/EntryListViewModel.cs +++ b/src/Artemis.UI/Screens/Workshop/Entries/List/EntryListViewModel.cs @@ -30,6 +30,7 @@ public partial class EntryListViewModel : RoutableScreen [Notify] private bool _initializing = true; [Notify] private bool _fetchingMore; [Notify] private int _entriesPerFetch; + [Notify] private bool _includeDefaultEntries; [Notify] private Vector _scrollOffset; protected EntryListViewModel(IWorkshopClient workshopClient, @@ -51,13 +52,14 @@ public partial class EntryListViewModel : RoutableScreen Entries = entries; // Respond to filter query input changes + this.WhenAnyValue(vm => vm.IncludeDefaultEntries).Skip(1).Throttle(TimeSpan.FromMilliseconds(200)).Subscribe(_ => Reset()); this.WhenActivated(d => { InputViewModel.WhenAnyValue(vm => vm.Search).Skip(1).Throttle(TimeSpan.FromMilliseconds(200)).Subscribe(_ => Reset()).DisposeWith(d); InputViewModel.WhenAnyValue(vm => vm.SortBy).Skip(1).Throttle(TimeSpan.FromMilliseconds(200)).Subscribe(_ => Reset()).DisposeWith(d); CategoriesViewModel.WhenAnyValue(vm => vm.CategoryFilters).Skip(1).Subscribe(_ => Reset()).DisposeWith(d); }); - + // Load entries when the view model is first activated this.WhenActivatedAsync(async _ => { @@ -76,7 +78,7 @@ public partial class EntryListViewModel : RoutableScreen public EntryType? EntryType { get; set; } public ReadOnlyObservableCollection Entries { get; } - + public async Task FetchMore(CancellationToken cancellationToken) { if (FetchingMore || _currentPageInfo != null && !_currentPageInfo.HasNextPage) @@ -122,6 +124,7 @@ public partial class EntryListViewModel : RoutableScreen And = [ new EntryFilterInput {EntryType = new EntryTypeOperationFilterInput {Eq = EntryType}}, + new EntryFilterInput {DefaultEntryInfo = IncludeDefaultEntries ? new DefaultEntryInfoFilterInput {EntryId = new LongOperationFilterInput {Ngt = 0}} : null}, ..CategoriesViewModel.CategoryFilters ?? [] ] }; diff --git a/src/Artemis.UI/Screens/Workshop/Library/Tabs/SubmissionsTabView.axaml b/src/Artemis.UI/Screens/Workshop/Library/Tabs/SubmissionsTabView.axaml index 63f5272e0..2fd59cde3 100644 --- a/src/Artemis.UI/Screens/Workshop/Library/Tabs/SubmissionsTabView.axaml +++ b/src/Artemis.UI/Screens/Workshop/Library/Tabs/SubmissionsTabView.axaml @@ -32,6 +32,7 @@ + diff --git a/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/SpecificationsStepViewModel.cs b/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/SpecificationsStepViewModel.cs index be4f864b9..593b5255c 100644 --- a/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/SpecificationsStepViewModel.cs +++ b/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/SpecificationsStepViewModel.cs @@ -80,7 +80,7 @@ public partial class SpecificationsStepViewModel : SubmissionViewModel if (State.Icon != null) { State.Icon.Seek(0, SeekOrigin.Begin); - viewModel.IconBitmap = BitmapExtensions.LoadAndResize(State.Icon, 128); + viewModel.IconBitmap = BitmapExtensions.LoadAndResize(State.Icon, 128, false); } EntrySpecificationsViewModel = viewModel; diff --git a/src/Artemis.WebClient.Workshop/WorkshopConstants.cs b/src/Artemis.WebClient.Workshop/WorkshopConstants.cs index 907ddb843..10807064c 100644 --- a/src/Artemis.WebClient.Workshop/WorkshopConstants.cs +++ b/src/Artemis.WebClient.Workshop/WorkshopConstants.cs @@ -2,10 +2,10 @@ namespace Artemis.WebClient.Workshop; public static class WorkshopConstants { - public const string AUTHORITY_URL = "https://localhost:5001"; - public const string WORKSHOP_URL = "https://localhost:7281"; - // public const string AUTHORITY_URL = "https://identity.artemis-rgb.com"; - // public const string WORKSHOP_URL = "https://workshop.artemis-rgb.com"; + // public const string AUTHORITY_URL = "https://localhost:5001"; + // public const string WORKSHOP_URL = "https://localhost:7281"; + public const string AUTHORITY_URL = "https://identity.artemis-rgb.com"; + public const string WORKSHOP_URL = "https://workshop.artemis-rgb.com"; public const string IDENTITY_CLIENT_NAME = "IdentityApiClient"; public const string WORKSHOP_CLIENT_NAME = "WorkshopApiClient"; } \ No newline at end of file diff --git a/src/Artemis.WebClient.Workshop/graphql.config.yml b/src/Artemis.WebClient.Workshop/graphql.config.yml index 4e6e409e5..f4c94896d 100644 --- a/src/Artemis.WebClient.Workshop/graphql.config.yml +++ b/src/Artemis.WebClient.Workshop/graphql.config.yml @@ -2,7 +2,7 @@ schema: schema.graphql extensions: endpoints: Default GraphQL Endpoint: - url: https://localhost:7281/graphql/ + url: https://workshop.artemis-rgb.com/graphql/ headers: user-agent: JS GraphQL introspect: true diff --git a/src/Artemis.WebClient.Workshop/schema.graphql b/src/Artemis.WebClient.Workshop/schema.graphql index 17190a026..3ca000dce 100644 --- a/src/Artemis.WebClient.Workshop/schema.graphql +++ b/src/Artemis.WebClient.Workshop/schema.graphql @@ -1,6 +1,4 @@ -# This file was generated. Do not edit manually. - -schema { +schema { query: Query mutation: Mutation } @@ -107,11 +105,19 @@ type Mutation { addEntry(input: CreateEntryInput!): Entry @authorize @cost(weight: "10") updateEntry(input: UpdateEntryInput!): Entry @authorize @cost(weight: "10") removeEntry(id: Long!): Entry @authorize @cost(weight: "10") - updateEntryImage(input: UpdateEntryImageInput!): Image @authorize @cost(weight: "10") - setLayoutInfo(input: SetLayoutInfoInput!): [LayoutInfo!]! @authorize @cost(weight: "10") - addLayoutInfo(input: CreateLayoutInfoInput!): LayoutInfo @authorize @cost(weight: "10") + updateEntryImage(input: UpdateEntryImageInput!): Image + @authorize + @cost(weight: "10") + setLayoutInfo(input: SetLayoutInfoInput!): [LayoutInfo!]! + @authorize + @cost(weight: "10") + addLayoutInfo(input: CreateLayoutInfoInput!): LayoutInfo + @authorize + @cost(weight: "10") removeLayoutInfo(id: Long!): LayoutInfo! @authorize @cost(weight: "10") - updateRelease(input: UpdateReleaseInput!): Release @authorize @cost(weight: "10") + updateRelease(input: UpdateReleaseInput!): Release + @authorize + @cost(weight: "10") removeRelease(id: Long!): Release! @authorize @cost(weight: "10") } @@ -152,16 +158,88 @@ type PluginInfosCollectionSegment { } type Query { - categories(order: [CategorySortInput!] @cost(weight: "10") where: CategoryFilterInput @cost(weight: "10")): [Category!]! @cost(weight: "10") - entries(skip: Int take: Int search: String popular: Boolean order: [EntrySortInput!] @cost(weight: "10") where: EntryFilterInput @cost(weight: "10")): EntriesCollectionSegment @listSize(assumedSize: 100, slicingArguments: [ "take" ], slicingArgumentDefaultValue: 10, sizedFields: [ "items" ], requireOneSlicingArgument: false) @cost(weight: "10") - entriesV2(search: String popular: Boolean "Returns the first _n_ elements from the list." first: Int "Returns the elements in the list that come after the specified cursor." after: String "Returns the last _n_ elements from the list." last: Int "Returns the elements in the list that come before the specified cursor." before: String order: [EntrySortInput!] @cost(weight: "10") where: EntryFilterInput @cost(weight: "10")): EntriesV2Connection @listSize(assumedSize: 100, slicingArguments: [ "first", "last" ], slicingArgumentDefaultValue: 10, sizedFields: [ "edges", "nodes" ], requireOneSlicingArgument: false) @cost(weight: "10") + categories( + order: [CategorySortInput!] @cost(weight: "10") + where: CategoryFilterInput @cost(weight: "10") + ): [Category!]! @cost(weight: "10") + entries( + skip: Int + take: Int + search: String + popular: Boolean + order: [EntrySortInput!] @cost(weight: "10") + where: EntryFilterInput @cost(weight: "10") + ): EntriesCollectionSegment + @listSize( + assumedSize: 100 + slicingArguments: ["take"] + slicingArgumentDefaultValue: 10 + sizedFields: ["items"] + requireOneSlicingArgument: false + ) + @cost(weight: "10") + entriesV2( + search: String + popular: Boolean + "Returns the first _n_ elements from the list." + first: Int + "Returns the elements in the list that come after the specified cursor." + after: String + "Returns the last _n_ elements from the list." + last: Int + "Returns the elements in the list that come before the specified cursor." + before: String + order: [EntrySortInput!] @cost(weight: "10") + where: EntryFilterInput @cost(weight: "10") + ): EntriesV2Connection + @listSize( + assumedSize: 100 + slicingArguments: ["first", "last"] + slicingArgumentDefaultValue: 10 + sizedFields: ["edges", "nodes"] + requireOneSlicingArgument: false + ) + @cost(weight: "10") entry(id: Long!): Entry @cost(weight: "10") - submittedEntries(order: [EntrySortInput!] @cost(weight: "10") where: EntryFilterInput @cost(weight: "10")): [Entry!]! @authorize @cost(weight: "10") - popularEntries(where: EntryFilterInput @cost(weight: "10")): [Entry!]! @cost(weight: "10") - searchEntries(input: String! type: EntryType order: [EntrySortInput!] @cost(weight: "10") where: EntryFilterInput @cost(weight: "10")): [Entry!]! @cost(weight: "10") - searchLayout(deviceProvider: UUID! deviceType: RGBDeviceType! vendor: String! model: String!): LayoutInfo @cost(weight: "10") - searchKeyboardLayout(deviceProvider: UUID! vendor: String! model: String! physicalLayout: KeyboardLayoutType! logicalLayout: String): LayoutInfo @cost(weight: "10") - pluginInfos(skip: Int take: Int order: [PluginInfoSortInput!] @cost(weight: "10") where: PluginInfoFilterInput @cost(weight: "10")): PluginInfosCollectionSegment @listSize(assumedSize: 100, slicingArguments: [ "take" ], slicingArgumentDefaultValue: 10, sizedFields: [ "items" ], requireOneSlicingArgument: false) @cost(weight: "10") + submittedEntries( + order: [EntrySortInput!] @cost(weight: "10") + where: EntryFilterInput @cost(weight: "10") + ): [Entry!]! @authorize @cost(weight: "10") + popularEntries(where: EntryFilterInput @cost(weight: "10")): [Entry!]! + @cost(weight: "10") + searchEntries( + input: String! + type: EntryType + order: [EntrySortInput!] @cost(weight: "10") + where: EntryFilterInput @cost(weight: "10") + ): [Entry!]! @cost(weight: "10") + searchLayout( + deviceProvider: UUID! + deviceType: RGBDeviceType! + vendor: String! + model: String! + ): LayoutInfo @cost(weight: "10") + searchKeyboardLayout( + deviceProvider: UUID! + vendor: String! + model: String! + physicalLayout: KeyboardLayoutType! + logicalLayout: String + ): LayoutInfo @cost(weight: "10") + pluginInfos( + skip: Int + take: Int + order: [PluginInfoSortInput!] @cost(weight: "10") + where: PluginInfoFilterInput @cost(weight: "10") + ): PluginInfosCollectionSegment + @listSize( + assumedSize: 100 + slicingArguments: ["take"] + slicingArgumentDefaultValue: 10 + sizedFields: ["items"] + requireOneSlicingArgument: false + ) + @cost(weight: "10") pluginInfo(pluginGuid: UUID!): PluginInfo @cost(weight: "10") release(id: Long!): Release @cost(weight: "10") } @@ -621,21 +699,45 @@ enum SortEnumType { } "The authorize directive." -directive @authorize("The name of the authorization policy that determines access to the annotated resource." policy: String "Roles that are allowed to access the annotated resource." roles: [String!] "Defines when when the authorize directive shall be applied.By default the authorize directives are applied during the validation phase." apply: ApplyPolicy! = BEFORE_RESOLVER) repeatable on OBJECT | FIELD_DEFINITION +directive @authorize( + "The name of the authorization policy that determines access to the annotated resource." + policy: String + "Roles that are allowed to access the annotated resource." + roles: [String!] + "Defines when when the authorize directive shall be applied.By default the authorize directives are applied during the validation phase." + apply: ApplyPolicy! = BEFORE_RESOLVER +) repeatable on OBJECT | FIELD_DEFINITION "The purpose of the `cost` directive is to define a `weight` for GraphQL types, fields, and arguments. Static analysis can use these weights when calculating the overall cost of a query or response." -directive @cost("The `weight` argument defines what value to add to the overall cost for every appearance, or possible appearance, of a type, field, argument, etc." weight: String!) on SCALAR | OBJECT | FIELD_DEFINITION | ARGUMENT_DEFINITION | ENUM | INPUT_FIELD_DEFINITION +directive @cost( + "The `weight` argument defines what value to add to the overall cost for every appearance, or possible appearance, of a type, field, argument, etc." + weight: String! +) on SCALAR | OBJECT | FIELD_DEFINITION | ARGUMENT_DEFINITION | ENUM | INPUT_FIELD_DEFINITION "The purpose of the `@listSize` directive is to either inform the static analysis about the size of returned lists (if that information is statically available), or to point the analysis to where to find that information." -directive @listSize("The `assumedSize` argument can be used to statically define the maximum length of a list returned by a field." assumedSize: Int "The `slicingArguments` argument can be used to define which of the field's arguments with numeric type are slicing arguments, so that their value determines the size of the list returned by that field. It may specify a list of multiple slicing arguments." slicingArguments: [String!] "The `slicingArgumentDefaultValue` argument can be used to define a default value for a slicing argument, which is used if the argument is not present in a query." slicingArgumentDefaultValue: Int "The `sizedFields` argument can be used to define that the value of the `assumedSize` argument or of a slicing argument does not affect the size of a list returned by a field itself, but that of a list returned by one of its sub-fields." sizedFields: [String!] "The `requireOneSlicingArgument` argument can be used to inform the static analysis that it should expect that exactly one of the defined slicing arguments is present in a query. If that is not the case (i.e., if none or multiple slicing arguments are present), the static analysis may throw an error." requireOneSlicingArgument: Boolean! = true) on FIELD_DEFINITION +directive @listSize( + "The `assumedSize` argument can be used to statically define the maximum length of a list returned by a field." + assumedSize: Int + "The `slicingArguments` argument can be used to define which of the field's arguments with numeric type are slicing arguments, so that their value determines the size of the list returned by that field. It may specify a list of multiple slicing arguments." + slicingArguments: [String!] + "The `slicingArgumentDefaultValue` argument can be used to define a default value for a slicing argument, which is used if the argument is not present in a query." + slicingArgumentDefaultValue: Int + "The `sizedFields` argument can be used to define that the value of the `assumedSize` argument or of a slicing argument does not affect the size of a list returned by a field itself, but that of a list returned by one of its sub-fields." + sizedFields: [String!] + "The `requireOneSlicingArgument` argument can be used to inform the static analysis that it should expect that exactly one of the defined slicing arguments is present in a query. If that is not the case (i.e., if none or multiple slicing arguments are present), the static analysis may throw an error." + requireOneSlicingArgument: Boolean! = true +) on FIELD_DEFINITION "The `@specifiedBy` directive is used within the type system definition language to provide a URL for specifying the behavior of custom scalar definitions." -directive @specifiedBy("The specifiedBy URL points to a human-readable specification. This field will only read a result for scalar types." url: String!) on SCALAR +directive @specifiedBy( + "The specifiedBy URL points to a human-readable specification. This field will only read a result for scalar types." + url: String! +) on SCALAR "The `DateTime` scalar represents an ISO-8601 compliant date time type." -scalar DateTime @specifiedBy(url: "https:\/\/www.graphql-scalars.com\/date-time") +scalar DateTime @specifiedBy(url: "https://www.graphql-scalars.com/date-time") "The `Long` scalar type represents non-fractional signed whole 64-bit numeric values. Long can represent values between -(2^63) and 2^63 - 1." scalar Long -scalar UUID @specifiedBy(url: "https:\/\/tools.ietf.org\/html\/rfc4122") \ No newline at end of file +scalar UUID @specifiedBy(url: "https://tools.ietf.org/html/rfc4122")