An activity interface
This commit is contained in:
parent
e987f215bb
commit
6e2fec1620
30 changed files with 847 additions and 10 deletions
113
src/PostIt/PostIt.Tests/ActivitiesPageViewModelTests.cs
Normal file
113
src/PostIt/PostIt.Tests/ActivitiesPageViewModelTests.cs
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
using System.Net.Http;
|
||||
using PostIt.ViewModels;
|
||||
using Yavsc.Abstract.Workflow;
|
||||
using Yavsc.Api.Client;
|
||||
|
||||
namespace PostIt.Tests;
|
||||
|
||||
public class ActivitiesPageViewModelTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task ActivityApiClient_uses_business_absolute_paths()
|
||||
{
|
||||
var api = new StubActivityApi();
|
||||
var client = new ActivityApiClient(api, "https://business.example/api/v1/");
|
||||
|
||||
await client.GetCatalogAsync("brush", TestContext.Current.CancellationToken);
|
||||
await client.GetPerformersAsync("brush-pro", TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.Equal("https://business.example/api/v1/activity/catalog?parentCode=brush", api.Paths[0]);
|
||||
Assert.Equal("https://business.example/api/v1/activity/brush-pro/performers", api.Paths[1]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RefreshAsync_loads_first_activity_then_specialization_performers()
|
||||
{
|
||||
var api = new StubActivityApi();
|
||||
var client = new ActivityApiClient(api, "https://business.example/api/v1/");
|
||||
var vm = new ActivitiesPageViewModel(client);
|
||||
|
||||
await vm.RefreshAsync();
|
||||
|
||||
Assert.Equal("brush", vm.SelectedActivity?.Code);
|
||||
Assert.Single(vm.Specializations);
|
||||
Assert.Equal("brush", vm.CurrentActivity?.Code);
|
||||
Assert.Single(vm.Performers);
|
||||
Assert.Equal("Alice", vm.Performers[0].UserName);
|
||||
|
||||
await vm.ShowSpecializationAsync(vm.Specializations[0]);
|
||||
|
||||
Assert.Equal("brush-pro", vm.CurrentActivity?.Code);
|
||||
Assert.Single(vm.Performers);
|
||||
Assert.Equal("Bob", vm.Performers[0].UserName);
|
||||
Assert.Contains("brush pro", vm.StatusMessage, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
await vm.ShowSpecializationAsync(null);
|
||||
|
||||
Assert.Equal("brush", vm.CurrentActivity?.Code);
|
||||
Assert.Single(vm.Performers);
|
||||
Assert.Equal("Alice", vm.Performers[0].UserName);
|
||||
}
|
||||
|
||||
private sealed class StubActivityApi : IYavscApiClient
|
||||
{
|
||||
public HttpClient Http { get; } = new();
|
||||
public List<string> Paths { get; } = new();
|
||||
|
||||
public Task<T> CallAsync<T>(HttpMethod method, string path, object? body = null, CancellationToken ct = default)
|
||||
{
|
||||
Paths.Add(path);
|
||||
|
||||
if (typeof(T) == typeof(List<ActivityBrowseItemDto>))
|
||||
{
|
||||
var activities = new List<ActivityBrowseItemDto>
|
||||
{
|
||||
new()
|
||||
{
|
||||
Code = "brush",
|
||||
Name = "Brush",
|
||||
Description = "Coiffure à domicile",
|
||||
PerformerCount = 1,
|
||||
Children = new List<ActivityBrowseItemDto>
|
||||
{
|
||||
new()
|
||||
{
|
||||
Code = "brush-pro",
|
||||
Name = "Brush Pro",
|
||||
Description = "Spécialisation premium",
|
||||
ParentCode = "brush",
|
||||
PerformerCount = 1,
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
return Task.FromResult((T)(object)activities);
|
||||
}
|
||||
|
||||
if (typeof(T) == typeof(List<ActivityPerformerDto>))
|
||||
{
|
||||
var performers = path.EndsWith("brush-pro/performers", StringComparison.Ordinal)
|
||||
? new List<ActivityPerformerDto>
|
||||
{
|
||||
new() { PerformerId = "pro-2", UserName = "Bob", ActivityCode = "brush-pro", ActivityName = "Brush Pro" }
|
||||
}
|
||||
: new List<ActivityPerformerDto>
|
||||
{
|
||||
new() { PerformerId = "pro-1", UserName = "Alice", ActivityCode = "brush", ActivityName = "Brush" }
|
||||
};
|
||||
|
||||
return Task.FromResult((T)(object)performers);
|
||||
}
|
||||
|
||||
return Task.FromResult(default(T)!);
|
||||
}
|
||||
|
||||
public Task CallAsync(HttpMethod method, string path, object? body = null, CancellationToken ct = default)
|
||||
{
|
||||
Paths.Add(path);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public ValueTask DisposeAsync() => ValueTask.CompletedTask;
|
||||
}
|
||||
}
|
||||
|
|
@ -94,6 +94,26 @@ public class SignaturePadControlTests
|
|||
Assert.NotEqual(first.Strokes, third.Strokes);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PendingStroke_is_exposed_only_while_capturing()
|
||||
{
|
||||
var pad = new SignaturePadControl();
|
||||
|
||||
Assert.Empty(pad.PendingStroke);
|
||||
|
||||
pad.BeginCaptureForTest();
|
||||
pad.AppendPointForTest(1_000, 2_000);
|
||||
pad.AppendPointForTest(3_000, 4_000);
|
||||
|
||||
Assert.Equal(new[] { 1_000, 2_000, 3_000, 4_000 }, pad.PendingStroke);
|
||||
Assert.Equal(new[] { 1_000, 2_000, 3_000, 4_000 }, pad.Strokes);
|
||||
|
||||
pad.SealStrokeForTest();
|
||||
|
||||
Assert.Empty(pad.PendingStroke);
|
||||
Assert.Equal(new[] { 2, 1_000, 2_000, 3_000, 4_000 }, pad.Strokes);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Clear_empties_buffer_and_raises_redraw()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -212,6 +212,17 @@ public class SignaturePadControl : TemplatedControl
|
|||
_pendingPoints++;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Test hook: mark the control as actively capturing so tests
|
||||
/// can exercise the live-preview path without synthetic pointer
|
||||
/// events.
|
||||
/// </summary>
|
||||
internal void BeginCaptureForTest()
|
||||
{
|
||||
_capturing = true;
|
||||
_pendingPoints = 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Test hook: seal the currently-pending stroke with a length
|
||||
/// prefix. Mirrors what <see cref="OnCaptureReleased"/> does at
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ public static class ServiceCollectionHelpers
|
|||
var circleClient = new CircleApiClient(api, settings.BlogsApiUrl);
|
||||
var blogAclClient = new BlogAclApiClient(api, settings.BlogsApiUrl);
|
||||
var userSearchClient = new UserSearchClient(api, settings.BlogsApiUrl);
|
||||
var activityClient = new ActivityApiClient(api, settings.BusinessApiUrl);
|
||||
var userDirectory = new UserDirectory(userSearchClient);
|
||||
|
||||
// Vues
|
||||
|
|
@ -45,6 +46,7 @@ public static class ServiceCollectionHelpers
|
|||
services.AddSingleton<HomePage>();
|
||||
services.AddSingleton<SignaturePage>();
|
||||
services.AddSingleton<CirclesPage>();
|
||||
services.AddSingleton<ActivitiesPage>();
|
||||
// ViewModels
|
||||
services.AddSingleton(settings);
|
||||
services.AddSingleton<YavscApiClient>(api);
|
||||
|
|
@ -52,10 +54,12 @@ public static class ServiceCollectionHelpers
|
|||
services.AddSingleton(circleClient);
|
||||
services.AddSingleton(blogAclClient);
|
||||
services.AddSingleton(userSearchClient);
|
||||
services.AddSingleton(activityClient);
|
||||
services.AddSingleton<IUserDirectory>(userDirectory);
|
||||
services.AddSingleton<HomePageViewModel>();
|
||||
services.AddSingleton<SignaturePageViewModel>();
|
||||
services.AddSingleton<CirclesPageViewModel>();
|
||||
services.AddSingleton<ActivitiesPageViewModel>();
|
||||
|
||||
// Dialogs (modal-light pages): the ViewLocator resolves
|
||||
// them when a caller pushes a PostAclDialogViewModel or
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ public class ViewLocator : IDataTemplate
|
|||
MainViewModel => services.GetRequiredService<MainPage>(),
|
||||
Settings => services.GetRequiredService<SettingsPage>(),
|
||||
HomePageViewModel => services.GetRequiredService<HomePage>(),
|
||||
ActivitiesPageViewModel => services.GetRequiredService<ActivitiesPage>(),
|
||||
SignaturePageViewModel => services.GetRequiredService<SignaturePage>(),
|
||||
AddCircleMemberDialogViewModel => services.GetRequiredService<AddCircleMemberDialog>(),
|
||||
CirclesPageViewModel => services.GetRequiredService<CirclesPage>(),
|
||||
|
|
|
|||
219
src/PostIt/PostIt/ViewModels/ActivitiesPageViewModel.cs
Normal file
219
src/PostIt/PostIt/ViewModels/ActivitiesPageViewModel.cs
Normal file
|
|
@ -0,0 +1,219 @@
|
|||
using System;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using Yavsc.Abstract.Workflow;
|
||||
using Yavsc.Api.Client;
|
||||
|
||||
namespace PostIt.ViewModels;
|
||||
|
||||
public partial class ActivitiesPageViewModel : ViewModelBase
|
||||
{
|
||||
private readonly ActivityApiClient _client;
|
||||
private bool _syncingSelection;
|
||||
|
||||
[ObservableProperty]
|
||||
public partial ObservableCollection<ActivityBrowseItemDto> Activities { get; set; } = new();
|
||||
|
||||
[ObservableProperty]
|
||||
public partial ActivityBrowseItemDto? SelectedActivity { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial ObservableCollection<ActivityBrowseItemDto> Specializations { get; set; } = new();
|
||||
|
||||
[ObservableProperty]
|
||||
public partial ActivityBrowseItemDto? SelectedSpecialization { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial ObservableCollection<ActivityPerformerDto> Performers { get; set; } = new();
|
||||
|
||||
[ObservableProperty]
|
||||
public partial bool IsBusy { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial string StatusMessage { get; set; } = "Choisissez une activité.";
|
||||
|
||||
public ActivityBrowseItemDto? CurrentActivity => SelectedSpecialization ?? SelectedActivity;
|
||||
public string SelectedActivityLabel => SelectedActivity?.Name ?? "(aucune activité)";
|
||||
public string CurrentActivityLabel => CurrentActivity?.Name ?? "(aucune)";
|
||||
|
||||
public override bool CanNavigateNext
|
||||
{
|
||||
get => false;
|
||||
protected set { _ = value; }
|
||||
}
|
||||
|
||||
public override bool CanNavigatePrevious
|
||||
{
|
||||
get => true;
|
||||
protected set { _ = value; }
|
||||
}
|
||||
|
||||
public ActivitiesPageViewModel(ActivityApiClient client)
|
||||
{
|
||||
_client = client ?? throw new ArgumentNullException(nameof(client));
|
||||
}
|
||||
|
||||
partial void OnSelectedActivityChanged(ActivityBrowseItemDto? value)
|
||||
{
|
||||
if (_syncingSelection) return;
|
||||
_ = ShowActivitySafeAsync(value);
|
||||
}
|
||||
|
||||
partial void OnSelectedSpecializationChanged(ActivityBrowseItemDto? value)
|
||||
{
|
||||
if (_syncingSelection) return;
|
||||
_ = ShowSpecializationSafeAsync(value);
|
||||
}
|
||||
|
||||
private async Task ShowActivitySafeAsync(ActivityBrowseItemDto? value)
|
||||
{
|
||||
try
|
||||
{
|
||||
await ShowActivityAsync(value);
|
||||
}
|
||||
catch (HttpRequestException ex) when (ex.StatusCode is HttpStatusCode.Unauthorized or HttpStatusCode.Forbidden)
|
||||
{
|
||||
StatusMessage = "Accès refusé pour les activités (scope 'api'). Déconnectez puis reconnectez-vous.";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
StatusMessage = $"Erreur: {ex.Message}";
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ShowSpecializationSafeAsync(ActivityBrowseItemDto? value)
|
||||
{
|
||||
try
|
||||
{
|
||||
await ShowSpecializationAsync(value);
|
||||
}
|
||||
catch (HttpRequestException ex) when (ex.StatusCode is HttpStatusCode.Unauthorized or HttpStatusCode.Forbidden)
|
||||
{
|
||||
StatusMessage = "Accès refusé pour les activités (scope 'api'). Déconnectez puis reconnectez-vous.";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
StatusMessage = $"Erreur: {ex.Message}";
|
||||
}
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
public async Task RefreshAsync()
|
||||
{
|
||||
IsBusy = true;
|
||||
try
|
||||
{
|
||||
var list = await _client.GetCatalogAsync();
|
||||
Activities = new ObservableCollection<ActivityBrowseItemDto>(list ?? new());
|
||||
|
||||
var first = Activities.FirstOrDefault();
|
||||
await ShowActivityAsync(first);
|
||||
if (first is null)
|
||||
{
|
||||
StatusMessage = "Aucune activité disponible.";
|
||||
}
|
||||
}
|
||||
catch (HttpRequestException ex) when (ex.StatusCode is HttpStatusCode.Unauthorized or HttpStatusCode.Forbidden)
|
||||
{
|
||||
Activities = new ObservableCollection<ActivityBrowseItemDto>();
|
||||
Specializations = new ObservableCollection<ActivityBrowseItemDto>();
|
||||
Performers = new ObservableCollection<ActivityPerformerDto>();
|
||||
StatusMessage = "Accès refusé pour les activités (scope 'api'). Déconnectez puis reconnectez-vous.";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Activities = new ObservableCollection<ActivityBrowseItemDto>();
|
||||
Specializations = new ObservableCollection<ActivityBrowseItemDto>();
|
||||
Performers = new ObservableCollection<ActivityPerformerDto>();
|
||||
StatusMessage = $"Erreur: {ex.Message}";
|
||||
}
|
||||
finally
|
||||
{
|
||||
IsBusy = false;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task ShowActivityAsync(ActivityBrowseItemDto? activity)
|
||||
{
|
||||
_syncingSelection = true;
|
||||
try
|
||||
{
|
||||
SelectedActivity = activity;
|
||||
SelectedSpecialization = null;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_syncingSelection = false;
|
||||
}
|
||||
|
||||
OnPropertyChanged(nameof(CurrentActivity));
|
||||
OnPropertyChanged(nameof(SelectedActivityLabel));
|
||||
OnPropertyChanged(nameof(CurrentActivityLabel));
|
||||
Specializations = new ObservableCollection<ActivityBrowseItemDto>(activity?.Children ?? new());
|
||||
|
||||
if (activity is null)
|
||||
{
|
||||
Performers = new ObservableCollection<ActivityPerformerDto>();
|
||||
return;
|
||||
}
|
||||
|
||||
await LoadPerformersAsync(activity);
|
||||
}
|
||||
|
||||
public async Task ShowSpecializationAsync(ActivityBrowseItemDto? specialization)
|
||||
{
|
||||
_syncingSelection = true;
|
||||
try
|
||||
{
|
||||
SelectedSpecialization = specialization;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_syncingSelection = false;
|
||||
}
|
||||
|
||||
OnPropertyChanged(nameof(CurrentActivity));
|
||||
OnPropertyChanged(nameof(CurrentActivityLabel));
|
||||
|
||||
if (specialization is null)
|
||||
{
|
||||
if (SelectedActivity is not null)
|
||||
{
|
||||
await LoadPerformersAsync(SelectedActivity);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
await LoadPerformersAsync(specialization);
|
||||
}
|
||||
|
||||
private async Task LoadPerformersAsync(ActivityBrowseItemDto activity)
|
||||
{
|
||||
IsBusy = true;
|
||||
try
|
||||
{
|
||||
var list = await _client.GetPerformersAsync(activity.Code);
|
||||
Performers = new ObservableCollection<ActivityPerformerDto>(list ?? new());
|
||||
StatusMessage = $"{activity.Name} · {Performers.Count} prestataire(s)";
|
||||
}
|
||||
catch (HttpRequestException ex) when (ex.StatusCode is HttpStatusCode.Unauthorized or HttpStatusCode.Forbidden)
|
||||
{
|
||||
Performers = new ObservableCollection<ActivityPerformerDto>();
|
||||
StatusMessage = "Accès refusé pour les activités (scope 'api'). Déconnectez puis reconnectez-vous.";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Performers = new ObservableCollection<ActivityPerformerDto>();
|
||||
StatusMessage = $"Erreur: {ex.Message}";
|
||||
}
|
||||
finally
|
||||
{
|
||||
IsBusy = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,9 @@
|
|||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Avalonia;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using PostIt.Helpers;
|
||||
using PostIt.Services;
|
||||
namespace PostIt.ViewModels;
|
||||
|
||||
|
|
@ -24,8 +29,25 @@ public class HomePageViewModel : ViewModelBase
|
|||
Settings = settings;
|
||||
SessionStatus = sessionStatus;
|
||||
|
||||
OpenActivities = new AsyncRelayCommand(OpenActivitiesAsync);
|
||||
|
||||
}
|
||||
public RelayCommand OpenBlogs { get; set; } = new RelayCommand(() => App.PushMainPageAsync());
|
||||
public IAsyncRelayCommand OpenBlogs { get; } = new AsyncRelayCommand(App.PushMainPageAsync);
|
||||
public IAsyncRelayCommand OpenActivities { get; }
|
||||
|
||||
private async Task OpenActivitiesAsync()
|
||||
{
|
||||
var app = (App?)Application.Current;
|
||||
var vm = app?.ServiceProvider?.GetRequiredService<ActivitiesPageViewModel>();
|
||||
if (app is null || vm is null)
|
||||
{
|
||||
throw new InvalidOperationException("Activities page is not available.");
|
||||
}
|
||||
|
||||
await vm.RefreshAsync();
|
||||
await app.PushPageAsync(vm);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Avalonia designer constructor. Builds a self-contained VM
|
||||
/// with a freshly-constructed Settings so the XAML preview can
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ public partial class Settings : ViewModelBase
|
|||
public partial string BlogsApiUrl { get; set; } = "https://blogs.pschneider.fr/api/v1/";
|
||||
|
||||
[ObservableProperty]
|
||||
public partial string BusinessApiUrl { get; set; } = "https://business.pschneider.fr/api/v1/";
|
||||
public partial string BusinessApiUrl { get; set; } = "https://api.pschneider.fr/api/v1/";
|
||||
|
||||
[ObservableProperty]
|
||||
public partial string SearchText { get; set; } = string.Empty;
|
||||
|
|
@ -154,7 +154,10 @@ public partial class Settings : ViewModelBase
|
|||
{
|
||||
"openid", // OIDC: required for the id_token
|
||||
"profile", // OIDC: standard profile claims
|
||||
"offline_access" // OIDC: required to receive a refresh_token
|
||||
"offline_access", // OIDC: required to receive a refresh_token
|
||||
"blogs",
|
||||
"api"
|
||||
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -297,8 +300,15 @@ public partial class Settings : ViewModelBase
|
|||
// → our overridden dispatcher-safe marshaller below.
|
||||
else lock (_mutationGate)
|
||||
{
|
||||
var legacyApiUrl = TryReadLegacyApiUrl(json);
|
||||
this.Authentication = settings.Authentication;
|
||||
this.DarkMode = settings.DarkMode;
|
||||
this.BlogsApiUrl = !string.IsNullOrWhiteSpace(settings.BlogsApiUrl)
|
||||
? settings.BlogsApiUrl
|
||||
: legacyApiUrl ?? this.BlogsApiUrl;
|
||||
this.BusinessApiUrl = !string.IsNullOrWhiteSpace(settings.BusinessApiUrl)
|
||||
? settings.BusinessApiUrl
|
||||
: this.BusinessApiUrl;
|
||||
this.SearchText = settings.SearchText ?? string.Empty;
|
||||
if (!(settings.Authentication is null))
|
||||
{
|
||||
|
|
@ -343,6 +353,26 @@ public partial class Settings : ViewModelBase
|
|||
}
|
||||
}
|
||||
|
||||
private static string? TryReadLegacyApiUrl(string json)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var doc = JsonDocument.Parse(json);
|
||||
if (doc.RootElement.TryGetProperty("ApiUrl", out var apiUrl)
|
||||
&& apiUrl.ValueKind == JsonValueKind.String)
|
||||
{
|
||||
return apiUrl.GetString();
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignore legacy payload parse errors: normal deserialization
|
||||
// already reports actionable diagnostics to the caller.
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private void UseDefaultSettings()
|
||||
{
|
||||
this.Authentication = new AuthenticationSettings
|
||||
|
|
@ -353,6 +383,8 @@ public partial class Settings : ViewModelBase
|
|||
Scopes = AuthenticationSettings.DefaultScopes
|
||||
};
|
||||
this.DarkMode = false;
|
||||
this.BlogsApiUrl = "https://blogs.pschneider.fr/api/v1/";
|
||||
this.BusinessApiUrl = "https://api.pschneider.fr/api/v1/";
|
||||
this.SearchText = string.Empty;
|
||||
}
|
||||
|
||||
|
|
|
|||
79
src/PostIt/PostIt/Views/ActivitiesPage.axaml
Normal file
79
src/PostIt/PostIt/Views/ActivitiesPage.axaml
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
<ContentPage xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:vm="using:PostIt.ViewModels"
|
||||
xmlns:wf="using:Yavsc.Abstract.Workflow"
|
||||
x:Class="PostIt.Views.ActivitiesPage"
|
||||
x:DataType="vm:ActivitiesPageViewModel"
|
||||
Header="Activités">
|
||||
<Grid RowDefinitions="Auto,*,Auto" Margin="12">
|
||||
<StackPanel Grid.Row="0" Orientation="Horizontal" Spacing="8">
|
||||
<Button Content="Rafraîchir" Command="{Binding RefreshCommand}" />
|
||||
<TextBlock Text="Catalogue des activités" FontWeight="Bold" VerticalAlignment="Center" />
|
||||
</StackPanel>
|
||||
|
||||
<Grid Grid.Row="1" Margin="0,12,0,12" ColumnDefinitions="*,16,*,16,*">
|
||||
<Grid Grid.Column="0" RowDefinitions="Auto,*">
|
||||
<TextBlock Grid.Row="0" Text="Activités" FontWeight="Bold" Margin="0,0,0,8" />
|
||||
<ListBox Grid.Row="1"
|
||||
ItemsSource="{Binding Activities}"
|
||||
SelectedItem="{Binding SelectedActivity, Mode=TwoWay}">
|
||||
<ListBox.ItemTemplate>
|
||||
<DataTemplate x:DataType="wf:ActivityBrowseItemDto">
|
||||
<StackPanel Spacing="2" Margin="0,0,0,8">
|
||||
<TextBlock Text="{Binding Name}" FontWeight="Bold" />
|
||||
<TextBlock Text="{Binding Description}" TextWrapping="Wrap" FontSize="11" Opacity="0.7" />
|
||||
<TextBlock Text="{Binding PerformerCount, StringFormat='Prestataires: {0}'}"
|
||||
FontSize="11" Opacity="0.6" />
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
</ListBox.ItemTemplate>
|
||||
</ListBox>
|
||||
</Grid>
|
||||
|
||||
<Grid Grid.Column="2" RowDefinitions="Auto,Auto,*">
|
||||
<TextBlock Grid.Row="0" Text="Spécialisations" FontWeight="Bold" Margin="0,0,0,8" />
|
||||
<TextBlock Grid.Row="1"
|
||||
Text="{Binding SelectedActivityLabel, StringFormat='Pour : {0}'}"
|
||||
FontSize="11" Opacity="0.7" Margin="0,0,0,8" />
|
||||
<ListBox Grid.Row="2"
|
||||
ItemsSource="{Binding Specializations}"
|
||||
SelectedItem="{Binding SelectedSpecialization, Mode=TwoWay}">
|
||||
<ListBox.ItemTemplate>
|
||||
<DataTemplate x:DataType="wf:ActivityBrowseItemDto">
|
||||
<StackPanel Spacing="2" Margin="0,0,0,8">
|
||||
<TextBlock Text="{Binding Name}" FontWeight="Bold" />
|
||||
<TextBlock Text="{Binding Description}" TextWrapping="Wrap" FontSize="11" Opacity="0.7" />
|
||||
<TextBlock Text="{Binding PerformerCount, StringFormat='Prestataires: {0}'}"
|
||||
FontSize="11" Opacity="0.6" />
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
</ListBox.ItemTemplate>
|
||||
</ListBox>
|
||||
</Grid>
|
||||
|
||||
<Grid Grid.Column="4" RowDefinitions="Auto,Auto,*">
|
||||
<TextBlock Grid.Row="0" Text="Prestataires" FontWeight="Bold" Margin="0,0,0,8" />
|
||||
<TextBlock Grid.Row="1"
|
||||
Text="{Binding CurrentActivityLabel, StringFormat='Activité affichée : {0}'}"
|
||||
FontSize="11" Opacity="0.7" Margin="0,0,0,8" />
|
||||
<ListBox Grid.Row="2" ItemsSource="{Binding Performers}">
|
||||
<ListBox.ItemTemplate>
|
||||
<DataTemplate x:DataType="wf:ActivityPerformerDto">
|
||||
<StackPanel Spacing="2" Margin="0,0,0,8">
|
||||
<TextBlock Text="{Binding UserName}" FontWeight="Bold" />
|
||||
<TextBlock Text="{Binding WebSite}" FontSize="11" Opacity="0.7" />
|
||||
<TextBlock Text="{Binding ExtraActivityCount, StringFormat='Autres spécialisations: {0}'}"
|
||||
FontSize="11" Opacity="0.6" />
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
</ListBox.ItemTemplate>
|
||||
</ListBox>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
<Grid Grid.Row="2" ColumnDefinitions="*,Auto">
|
||||
<TextBlock Grid.Column="0" Text="{Binding StatusMessage}" VerticalAlignment="Center" />
|
||||
<ProgressBar Grid.Column="1" Width="120" IsIndeterminate="True" IsVisible="{Binding IsBusy}" />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</ContentPage>
|
||||
17
src/PostIt/PostIt/Views/ActivitiesPage.axaml.cs
Normal file
17
src/PostIt/PostIt/Views/ActivitiesPage.axaml.cs
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
using Avalonia.Controls;
|
||||
using Avalonia.Markup.Xaml;
|
||||
|
||||
namespace PostIt.Views;
|
||||
|
||||
public partial class ActivitiesPage : ContentPage
|
||||
{
|
||||
public ActivitiesPage()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
private void InitializeComponent()
|
||||
{
|
||||
AvaloniaXamlLoader.Load(this);
|
||||
}
|
||||
}
|
||||
|
|
@ -18,5 +18,9 @@
|
|||
Command="{Binding OpenBlogs}"
|
||||
HorizontalAlignment="Center"
|
||||
IsEnabled="{Binding SessionStatus.IsLoggedIn}"/>
|
||||
<Button Content="Parcourir les activités"
|
||||
Command="{Binding OpenActivities}"
|
||||
HorizontalAlignment="Center"
|
||||
IsEnabled="{Binding SessionStatus.IsLoggedIn}"/>
|
||||
</StackPanel>
|
||||
</ContentPage>
|
||||
|
|
|
|||
|
|
@ -62,9 +62,16 @@ public partial class SignaturePage : ContentPage
|
|||
var h = PadFrame.Bounds.Height;
|
||||
if (w <= 0 || h <= 0) return;
|
||||
|
||||
var pending = _control.PendingStroke;
|
||||
var strokes = _control.Strokes;
|
||||
int sealedCount = strokes.Count - pending.Count;
|
||||
if (sealedCount < 0)
|
||||
{
|
||||
sealedCount = 0;
|
||||
}
|
||||
|
||||
int i = 0;
|
||||
while (i < strokes.Count)
|
||||
while (i < sealedCount)
|
||||
{
|
||||
int k = strokes[i];
|
||||
if (k <= 0) break;
|
||||
|
|
@ -88,7 +95,6 @@ public partial class SignaturePage : ContentPage
|
|||
InkLayer.Children.Add(poly);
|
||||
}
|
||||
|
||||
var pending = _control.PendingStroke;
|
||||
if (pending.Count > 0)
|
||||
{
|
||||
var poly = new Polyline
|
||||
|
|
|
|||
17
src/Yavsc.Abstract/Workflow/ActivityBrowseItemDto.cs
Normal file
17
src/Yavsc.Abstract/Workflow/ActivityBrowseItemDto.cs
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
namespace Yavsc.Abstract.Workflow;
|
||||
|
||||
/// <summary>
|
||||
/// Activity node returned by the browsing API.
|
||||
/// </summary>
|
||||
public sealed class ActivityBrowseItemDto
|
||||
{
|
||||
public string Code { get; set; } = string.Empty;
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public string ParentCode { get; set; } = string.Empty;
|
||||
public string Description { get; set; } = string.Empty;
|
||||
public string Photo { get; set; } = string.Empty;
|
||||
public int Rate { get; set; }
|
||||
public int PerformerCount { get; set; }
|
||||
public List<CommandFormSummaryDto> Forms { get; set; } = new();
|
||||
public List<ActivityBrowseItemDto> Children { get; set; } = new();
|
||||
}
|
||||
18
src/Yavsc.Abstract/Workflow/ActivityPerformerDto.cs
Normal file
18
src/Yavsc.Abstract/Workflow/ActivityPerformerDto.cs
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
namespace Yavsc.Abstract.Workflow;
|
||||
|
||||
/// <summary>
|
||||
/// Lightweight performer description returned for one activity.
|
||||
/// </summary>
|
||||
public sealed class ActivityPerformerDto
|
||||
{
|
||||
public string PerformerId { get; set; } = string.Empty;
|
||||
public string UserName { get; set; } = string.Empty;
|
||||
public bool Active { get; set; }
|
||||
public bool AcceptNotifications { get; set; }
|
||||
public bool AcceptPublicContact { get; set; }
|
||||
public string WebSite { get; set; } = string.Empty;
|
||||
public string ActivityCode { get; set; } = string.Empty;
|
||||
public string ActivityName { get; set; } = string.Empty;
|
||||
public string SettingsClassName { get; set; } = string.Empty;
|
||||
public int ExtraActivityCount { get; set; }
|
||||
}
|
||||
11
src/Yavsc.Abstract/Workflow/CommandFormSummaryDto.cs
Normal file
11
src/Yavsc.Abstract/Workflow/CommandFormSummaryDto.cs
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
namespace Yavsc.Abstract.Workflow;
|
||||
|
||||
/// <summary>
|
||||
/// Lightweight command-form description exposed to API clients.
|
||||
/// </summary>
|
||||
public sealed class CommandFormSummaryDto
|
||||
{
|
||||
public long Id { get; set; }
|
||||
public string ActionName { get; set; } = string.Empty;
|
||||
public string Title { get; set; } = string.Empty;
|
||||
}
|
||||
56
src/Yavsc.Api.Client/ActivityApiClient.cs
Normal file
56
src/Yavsc.Api.Client/ActivityApiClient.cs
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Yavsc.Abstract.Workflow;
|
||||
|
||||
namespace Yavsc.Api.Client;
|
||||
|
||||
/// <summary>
|
||||
/// HTTP client for browsing business activities and their performers.
|
||||
/// Uses absolute URLs so it can coexist with other Yavsc clients that
|
||||
/// target a different API host on the same shared transport.
|
||||
/// </summary>
|
||||
public sealed class ActivityApiClient
|
||||
{
|
||||
private const string PathPrefix = "activity";
|
||||
|
||||
private readonly IYavscApiClient _api;
|
||||
private readonly Uri _baseAddress;
|
||||
|
||||
public ActivityApiClient(IYavscApiClient api, string businessBaseAddress)
|
||||
{
|
||||
_api = api ?? throw new ArgumentNullException(nameof(api));
|
||||
if (string.IsNullOrEmpty(businessBaseAddress))
|
||||
throw new ArgumentException("Base address is required.", nameof(businessBaseAddress));
|
||||
|
||||
_baseAddress = new Uri(businessBaseAddress, UriKind.Absolute);
|
||||
}
|
||||
|
||||
public Task<List<ActivityBrowseItemDto>> GetCatalogAsync(
|
||||
string? parentCode = null,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
var path = string.IsNullOrWhiteSpace(parentCode)
|
||||
? $"{PathPrefix}/catalog"
|
||||
: $"{PathPrefix}/catalog?parentCode={Uri.EscapeDataString(parentCode)}";
|
||||
|
||||
return _api.CallAsync<List<ActivityBrowseItemDto>>(HttpMethod.Get, Absolute(path), ct: ct);
|
||||
}
|
||||
|
||||
public Task<List<ActivityPerformerDto>> GetPerformersAsync(
|
||||
string activityCode,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(activityCode))
|
||||
throw new ArgumentException("Activity code is required.", nameof(activityCode));
|
||||
|
||||
return _api.CallAsync<List<ActivityPerformerDto>>(
|
||||
HttpMethod.Get,
|
||||
Absolute($"{PathPrefix}/{Uri.EscapeDataString(activityCode)}/performers"),
|
||||
ct: ct);
|
||||
}
|
||||
|
||||
private string Absolute(string relativePath) => new Uri(_baseAddress, relativePath).ToString();
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Yavsc.Abstract.Workflow;
|
||||
using Yavsc.Server.Helpers;
|
||||
using Yavsc.Models;
|
||||
using Yavsc.Models.Workflow;
|
||||
|
|
@ -8,6 +9,7 @@ using Yavsc.Models.Workflow;
|
|||
|
||||
namespace Yavsc.Controllers
|
||||
{
|
||||
[Authorize]
|
||||
[Produces("application/json")]
|
||||
[Route(Constants.APIPrefix + "/activity")]
|
||||
public class ActivityApiController : Controller
|
||||
|
|
@ -26,6 +28,86 @@ namespace Yavsc.Controllers
|
|||
return _context.Activities.Include(a=>a.Forms).Where( a => !a.Hidden );
|
||||
}
|
||||
|
||||
[HttpGet("catalog")]
|
||||
public async Task<ActionResult<IEnumerable<ActivityBrowseItemDto>>> GetCatalog(
|
||||
CancellationToken cancellationToken,
|
||||
[FromQuery] string parentCode = null)
|
||||
{
|
||||
var activities = await _context.Activities
|
||||
.AsNoTracking()
|
||||
.Include(a => a.Forms)
|
||||
.Include(a => a.Children)
|
||||
.ThenInclude(c => c.Forms)
|
||||
.Where(a => !a.Hidden && a.ParentCode == parentCode)
|
||||
.OrderByDescending(a => a.Rate)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
var codes = activities
|
||||
.Select(a => a.Code)
|
||||
.Concat(activities.SelectMany(a => a.Children.Where(c => !c.Hidden).Select(c => c.Code)))
|
||||
.Distinct()
|
||||
.ToArray();
|
||||
|
||||
var performerCounts = await _context.Performers
|
||||
.AsNoTracking()
|
||||
.Where(p => p.Active)
|
||||
.SelectMany(
|
||||
p => p.Activity
|
||||
.Where(a => codes.Contains(a.DoesCode))
|
||||
.Select(a => new { a.DoesCode, p.PerformerId }))
|
||||
.GroupBy(x => x.DoesCode)
|
||||
.Select(g => new { Code = g.Key, Count = g.Select(x => x.PerformerId).Distinct().Count() })
|
||||
.ToDictionaryAsync(x => x.Code, x => x.Count, cancellationToken);
|
||||
|
||||
return Ok(activities.Select(a => ToBrowseItem(a, performerCounts)).ToList());
|
||||
}
|
||||
|
||||
[HttpGet("{id}/performers")]
|
||||
public async Task<ActionResult<IEnumerable<ActivityPerformerDto>>> GetPerformers(
|
||||
[FromRoute] string id,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(id))
|
||||
{
|
||||
return BadRequest("Activity code is required.");
|
||||
}
|
||||
|
||||
var activity = await _context.Activities
|
||||
.AsNoTracking()
|
||||
.SingleOrDefaultAsync(a => a.Code == id, cancellationToken);
|
||||
if (activity is null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
var performers = await _context.Performers
|
||||
.AsNoTracking()
|
||||
.Include(p => p.Performer)
|
||||
.Include(p => p.Activity)
|
||||
.ThenInclude(a => a.Does)
|
||||
.Where(p => p.Active && p.Activity.Any(a => a.DoesCode == id))
|
||||
.OrderBy(p => p.Rate)
|
||||
.Select(p => new ActivityPerformerDto
|
||||
{
|
||||
PerformerId = p.PerformerId,
|
||||
UserName = p.Performer.UserName,
|
||||
Active = p.Active,
|
||||
AcceptNotifications = p.AcceptNotifications,
|
||||
AcceptPublicContact = p.AcceptPublicContact,
|
||||
WebSite = p.WebSite,
|
||||
ActivityCode = id,
|
||||
ActivityName = activity.Name,
|
||||
SettingsClassName = p.Activity
|
||||
.Where(a => a.DoesCode == id)
|
||||
.Select(a => a.Does.SettingsClassName)
|
||||
.FirstOrDefault(),
|
||||
ExtraActivityCount = p.Activity.Count(a => a.DoesCode != id)
|
||||
})
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
return Ok(performers);
|
||||
}
|
||||
|
||||
// GET: api/ActivityApi/5
|
||||
[HttpGet("{id}", Name = "GetActivity")]
|
||||
public async Task<IActionResult> GetActivity([FromRoute] string id)
|
||||
|
|
@ -144,5 +226,51 @@ namespace Yavsc.Controllers
|
|||
{
|
||||
return _context.Activities.Count(e => e.Code == id) > 0;
|
||||
}
|
||||
|
||||
private static ActivityBrowseItemDto ToBrowseItem(
|
||||
Activity activity,
|
||||
IReadOnlyDictionary<string, int> performerCounts)
|
||||
{
|
||||
return new ActivityBrowseItemDto
|
||||
{
|
||||
Code = activity.Code,
|
||||
Name = activity.Name,
|
||||
ParentCode = activity.ParentCode,
|
||||
Description = activity.Description,
|
||||
Photo = activity.Photo,
|
||||
Rate = activity.Rate,
|
||||
PerformerCount = performerCounts.TryGetValue(activity.Code, out var count) ? count : 0,
|
||||
Forms = activity.Forms
|
||||
.Select(f => new CommandFormSummaryDto
|
||||
{
|
||||
Id = f.Id,
|
||||
ActionName = f.ActionName,
|
||||
Title = f.Title,
|
||||
})
|
||||
.ToList(),
|
||||
Children = activity.Children
|
||||
.Where(c => !c.Hidden)
|
||||
.OrderByDescending(c => c.Rate)
|
||||
.Select(c => new ActivityBrowseItemDto
|
||||
{
|
||||
Code = c.Code,
|
||||
Name = c.Name,
|
||||
ParentCode = c.ParentCode,
|
||||
Description = c.Description,
|
||||
Photo = c.Photo,
|
||||
Rate = c.Rate,
|
||||
PerformerCount = performerCounts.TryGetValue(c.Code, out var childCount) ? childCount : 0,
|
||||
Forms = c.Forms
|
||||
.Select(f => new CommandFormSummaryDto
|
||||
{
|
||||
Id = f.Id,
|
||||
ActionName = f.ActionName,
|
||||
Title = f.Title,
|
||||
})
|
||||
.ToList(),
|
||||
})
|
||||
.ToList(),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ namespace Yavsc.ApiControllers
|
|||
using Yavsc.ViewModels.Auth;
|
||||
using Yavsc.Server.Helpers;
|
||||
|
||||
[Authorize]
|
||||
[Route(Constants.APIPrefix + "/bill"), Authorize]
|
||||
public class BillingController : Controller
|
||||
{
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ namespace Yavsc.Controllers
|
|||
using Microsoft.EntityFrameworkCore;
|
||||
using Yavsc.Server.Helpers;
|
||||
|
||||
[Authorize]
|
||||
[Produces("application/json")]
|
||||
[Route(Constants.APIPrefix + "/bookquery"), Authorize("Performer")]
|
||||
public class BookQueryApiController : Controller
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ using Yavsc.Server.Helpers;
|
|||
|
||||
namespace Yavsc.Controllers
|
||||
{
|
||||
[Authorize]
|
||||
[Produces("application/json")]
|
||||
[Route(Constants.APIPrefix + "/estimate"), Authorize]
|
||||
public class EstimateApiController : Controller
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
using System.Security.Claims;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Yavsc.Models;
|
||||
|
|
@ -7,6 +8,7 @@ using Yavsc.Server.Helpers;
|
|||
|
||||
namespace Yavsc.Controllers
|
||||
{
|
||||
[Authorize]
|
||||
[Produces("application/json")]
|
||||
[Route(Constants.APIPrefix + "/EstimateTemplatesApi")]
|
||||
public class EstimateTemplatesApiController : Controller
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Yavsc.Helpers;
|
||||
using Yavsc.Models;
|
||||
|
|
@ -6,6 +7,7 @@ using Yavsc.ViewModels.FrontOffice;
|
|||
|
||||
namespace Yavsc.ApiControllers
|
||||
{
|
||||
[Authorize]
|
||||
[Route(Constants.APIPrefix + "/front")]
|
||||
public class FrontOfficeApiController : Controller
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Newtonsoft.Json;
|
||||
|
|
@ -6,6 +7,7 @@ using Yavsc.Models;
|
|||
|
||||
namespace Yavsc.ApiControllers
|
||||
{
|
||||
[Authorize]
|
||||
[Route(Constants.APIPrefix + "/payment")]
|
||||
public class PaymentApiController : Controller
|
||||
{
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ namespace Yavsc.Controllers
|
|||
using Yavsc.Helpers;
|
||||
using Yavsc.Services;
|
||||
|
||||
[Authorize]
|
||||
[Produces("application/json")]
|
||||
[Route(Constants.APIPrefix + "/performers")]
|
||||
public class PerformersApiController : Controller
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ using Yavsc.Server.Helpers;
|
|||
|
||||
namespace Yavsc.Controllers
|
||||
{
|
||||
[Authorize]
|
||||
[Produces("application/json")]
|
||||
[Route(Constants.APIPrefix + "/ProductApi")]
|
||||
public class ProductApiController : Controller
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ public static class Constants
|
|||
// 'offline_access' is handled by IdentityServer8 itself and never
|
||||
// needs an explicit ApiScope row.
|
||||
public static readonly string[] BuildInApiScopes = {
|
||||
"admin", "moderation", "performer", "client" };
|
||||
"admin", "moderation", "performer", "client", "api" };
|
||||
|
||||
// One ApiResource per application scope. Each scope is exposed by
|
||||
// exactly one resource, named after the scope ("admin" -> "admin"
|
||||
|
|
@ -29,6 +29,7 @@ public static class Constants
|
|||
new ApiResourceScopeSpecification { ScopeName = "moderation", Description = "Moderation access", ResourceName = "moderation", ResourceDisplayName = "Moderation API" },
|
||||
new ApiResourceScopeSpecification { ScopeName = "performer", Description = "Performer access", ResourceName = "performer", ResourceDisplayName = "Performer API" },
|
||||
new ApiResourceScopeSpecification { ScopeName = "client", Description = "Client access", ResourceName = "client", ResourceDisplayName = "Client API" },
|
||||
new ApiResourceScopeSpecification { ScopeName = "api", Description = "Core API access", ResourceName = "api", ResourceDisplayName = "Yavsc Core API" },
|
||||
new ApiResourceScopeSpecification { ScopeName = "blogs", Description = "Blogs access", ResourceName = "blogs", ResourceDisplayName = "Yavsc Blogs API" }
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -803,6 +803,7 @@ public static class HostingExtensions
|
|||
// silent refresh path to work; without it IdentityServer
|
||||
// refuses to issue a refresh_token.
|
||||
"blogs",
|
||||
"api",
|
||||
IdentityServer8.IdentityServerConstants.StandardScopes.OpenId,
|
||||
IdentityServer8.IdentityServerConstants.StandardScopes.Profile,
|
||||
IdentityServer8.IdentityServerConstants.StandardScopes.OfflineAccess,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue