feat/estimate #50
12 changed files with 160 additions and 23 deletions
commit
45d3f1f4f8
20
CHANGELOG.md
20
CHANGELOG.md
|
|
@ -1,5 +1,25 @@
|
|||
# Changelog
|
||||
|
||||
## [1.0.8-rc12] - unstable
|
||||
|
||||
### Added
|
||||
|
||||
* [PostIt] Nouveau helper d'image `ImageHelper` pour charger des bitmaps depuis les ressources et depuis le web.
|
||||
* [PostIt] Affichage de l'avatar XS dans la liste des performers d'activites, avec fallback visuel (initiale utilisateur).
|
||||
* [PostIt.Tests] Nouveaux tests autour des URLs avatar et de la source d'autorite.
|
||||
* [contrib] Ajout d'un `README.md` utilitaire pour les symboles/icones.
|
||||
|
||||
### Changed
|
||||
|
||||
* [PostIt] Les avatars ne sont plus relies en string sur `Image.Source`: ils sont telecharges et lies en `Bitmap`.
|
||||
* [Yavsc.Api.Client] `ActivityApiClient` accepte une base d'avatar dediee et construit les URLs avatar depuis l'autorite d'identification.
|
||||
* [PostIt] Le header de `MainPage` n'utilise plus `ScrollViewer`; remplacement par une barre de commandes basee sur `WrapPanel`.
|
||||
* [PostIt] Alignement de la navigation blogs: renommage `PushMainPageAsync` -> `PushBlogsPageAsync` et ajustement de `HomePageViewModel`.
|
||||
|
||||
### Fixed
|
||||
|
||||
néant
|
||||
|
||||
## [1.0.8-rc11] - unstable
|
||||
|
||||
### Added
|
||||
|
|
|
|||
5
contrib/README.md
Normal file
5
contrib/README.md
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
# Read me
|
||||
|
||||
## Note aux icones
|
||||
|
||||
㝉®🅬⛒⛑🩎🩺🞫🞮🞕🞖🞆🔴🔵🔲🖂🔧🔩🔐🔌💾💼💬💭👿👾🏷🎯🏹🌍🎎💩
|
||||
|
|
@ -7,6 +7,20 @@ namespace PostIt.Tests;
|
|||
|
||||
public class ActivitiesPageViewModelTests
|
||||
{
|
||||
[Fact]
|
||||
public void ActivityApiClient_uses_avatar_authority_when_provided()
|
||||
{
|
||||
var api = new StubActivityApi();
|
||||
var client = new ActivityApiClient(
|
||||
api,
|
||||
"https://api.pschneider.fr/api/v1/",
|
||||
"https://yavsc.pschneider.fr/");
|
||||
|
||||
var url = client.BuildAvatarXsUrl("paul");
|
||||
|
||||
Assert.Equal("https://yavsc.pschneider.fr/avatars/paul.xs.png", url);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ActivityApiClient_uses_business_absolute_paths()
|
||||
{
|
||||
|
|
@ -40,6 +54,7 @@ public class ActivitiesPageViewModelTests
|
|||
Assert.Equal("brush", vm.CurrentActivity?.Code);
|
||||
Assert.Single(vm.Performers);
|
||||
Assert.Equal("Alice", vm.Performers[0].UserName);
|
||||
Assert.Equal("https://business.example/avatars/Alice.xs.png", vm.Performers[0].AvatarXsUrl);
|
||||
Assert.True(vm.Performers[0].HasPerformerProfile);
|
||||
Assert.True(vm.Performers[0].IsPerformerActive);
|
||||
Assert.Equal("Actif", vm.Performers[0].PerformerStatusBadgeLabel);
|
||||
|
|
@ -50,6 +65,7 @@ public class ActivitiesPageViewModelTests
|
|||
Assert.Equal("brush-pro", vm.CurrentActivity?.Code);
|
||||
Assert.Single(vm.Performers);
|
||||
Assert.Equal("Bob", vm.Performers[0].UserName);
|
||||
Assert.Equal("https://business.example/avatars/Bob.xs.png", vm.Performers[0].AvatarXsUrl);
|
||||
Assert.True(vm.Performers[0].HasPerformerProfile);
|
||||
Assert.False(vm.Performers[0].IsPerformerActive);
|
||||
Assert.Equal("Inactif", vm.Performers[0].PerformerStatusBadgeLabel);
|
||||
|
|
|
|||
|
|
@ -135,9 +135,6 @@ private void ConfigureRootView(MainView rootView)
|
|||
var homePage = provider.GetRequiredService<HomePageViewModel>();
|
||||
var app = (App)Current!;
|
||||
await app.PushPageAsync(homePage);
|
||||
if (!refreshed) return;
|
||||
|
||||
await PushMainPageAsync().ConfigureAwait(true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -148,7 +145,7 @@ private void ConfigureRootView(MainView rootView)
|
|||
/// (interactive login from the banner). Pulled out as a helper so
|
||||
/// the two callers can't drift apart.
|
||||
/// </summary>
|
||||
public static async Task PushMainPageAsync()
|
||||
public static async Task PushBlogsPageAsync()
|
||||
{
|
||||
var app = (App)Current!;
|
||||
var mainVm = app.ServiceProvider!.GetRequiredService<MainViewModel>();
|
||||
|
|
|
|||
34
src/PostIt/PostIt/Helpers/ImageHelper.cs
Normal file
34
src/PostIt/PostIt/Helpers/ImageHelper.cs
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
using System;
|
||||
using System.IO;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
using Avalonia.Media.Imaging;
|
||||
using Avalonia.Platform;
|
||||
|
||||
namespace PostIt.Helpers;
|
||||
|
||||
public static class ImageHelper
|
||||
{
|
||||
private static readonly HttpClient HttpClient = new();
|
||||
|
||||
public static Bitmap LoadFromResource(Uri resourceUri)
|
||||
{
|
||||
return new Bitmap(AssetLoader.Open(resourceUri));
|
||||
}
|
||||
|
||||
public static async Task<Bitmap?> LoadFromWeb(Uri url)
|
||||
{
|
||||
try
|
||||
{
|
||||
var response = await HttpClient.GetAsync(url).ConfigureAwait(false);
|
||||
response.EnsureSuccessStatusCode();
|
||||
var data = await response.Content.ReadAsByteArrayAsync().ConfigureAwait(false);
|
||||
return new Bitmap(new MemoryStream(data));
|
||||
}
|
||||
catch (HttpRequestException ex)
|
||||
{
|
||||
Console.WriteLine($"An error occurred while downloading image '{url}': {ex.Message}");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -24,7 +24,10 @@ 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.ApiUrl);
|
||||
var activityClient = new ActivityApiClient(
|
||||
api,
|
||||
settings.ApiUrl,
|
||||
settings.Authentication?.Authority);
|
||||
var billingClient = new BillingApiClient(api, settings.ApiUrl);
|
||||
var userDirectory = new UserDirectory(userSearchClient);
|
||||
|
||||
|
|
|
|||
|
|
@ -209,8 +209,26 @@ public partial class ActivitiesPageViewModel : ViewModelBase
|
|||
try
|
||||
{
|
||||
var list = await _client.GetUsersAsync(activity.Code);
|
||||
Performers = new ObservableCollection<ActivityUserDisplayItem>((list ?? new())
|
||||
.Select(ActivityUserDisplayItem.FromDto));
|
||||
var items = (list ?? new())
|
||||
.Select(dto => ActivityUserDisplayItem.FromDto(dto, _client.BuildAvatarXsUrl(dto.UserName)))
|
||||
.ToList();
|
||||
|
||||
await Task.WhenAll(items.Select(async item =>
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(item.AvatarXsUrl))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Uri.TryCreate(item.AvatarXsUrl, UriKind.Absolute, out var avatarUri))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
item.AvatarImage = await ImageHelper.LoadFromWeb(avatarUri);
|
||||
}));
|
||||
|
||||
Performers = new ObservableCollection<ActivityUserDisplayItem>(items);
|
||||
SelectedPerformer = null;
|
||||
StatusMessage = $"{activity.Name} · {Performers.Count} utilisateur(s)";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,13 @@
|
|||
using Avalonia.Media.Imaging;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using Yavsc.Abstract.Workflow;
|
||||
|
||||
namespace PostIt.ViewModels;
|
||||
|
||||
public sealed class ActivityUserDisplayItem
|
||||
public sealed partial class ActivityUserDisplayItem : ObservableObject
|
||||
{
|
||||
public string PerformerId { get; init; } = string.Empty;
|
||||
public string AvatarXsUrl { get; init; } = string.Empty;
|
||||
public bool HasPerformerProfile { get; init; }
|
||||
public string PerformerBadgeLabel { get; init; } = "Profil pro";
|
||||
public bool IsPerformerActive { get; init; }
|
||||
|
|
@ -13,17 +16,25 @@ public sealed class ActivityUserDisplayItem
|
|||
public string PerformerStatusBadgeBorder { get; init; } = "#C62828";
|
||||
public string PerformerStatusBadgeForeground { get; init; } = "#8E0000";
|
||||
public string UserName { get; init; } = string.Empty;
|
||||
public string AvatarFallbackLabel { get; init; } = "?";
|
||||
public string WebSite { get; init; } = string.Empty;
|
||||
public int ExtraActivityCount { get; init; }
|
||||
public string ExtraActivityLabel { get; init; } = "Pas d'autre activité";
|
||||
|
||||
public static ActivityUserDisplayItem FromDto(PerformerActivity dto)
|
||||
[ObservableProperty]
|
||||
public partial Bitmap? AvatarImage { get; set; }
|
||||
|
||||
public static ActivityUserDisplayItem FromDto(PerformerActivity dto, string avatarXsUrl)
|
||||
{
|
||||
return new ActivityUserDisplayItem
|
||||
{
|
||||
PerformerId = dto.PerformerId,
|
||||
AvatarXsUrl = avatarXsUrl,
|
||||
HasPerformerProfile = dto.HasPerformerProfile,
|
||||
UserName = dto.UserName,
|
||||
AvatarFallbackLabel = string.IsNullOrWhiteSpace(dto.UserName)
|
||||
? "?"
|
||||
: dto.UserName.Trim()[0].ToString().ToUpperInvariant(),
|
||||
WebSite = dto.WebSite,
|
||||
IsPerformerActive = dto.Active,
|
||||
PerformerStatusBadgeLabel = dto.Active ? "Actif" : "Inactif",
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ public class HomePageViewModel : ViewModelBase
|
|||
OpenActivities = new AsyncRelayCommand(OpenActivitiesAsync);
|
||||
|
||||
}
|
||||
public IAsyncRelayCommand OpenBlogs { get; } = new AsyncRelayCommand(App.PushMainPageAsync);
|
||||
public IAsyncRelayCommand OpenBlogs { get; } = new AsyncRelayCommand(App.PushBlogsPageAsync);
|
||||
public IAsyncRelayCommand OpenActivities { get; }
|
||||
|
||||
private async Task OpenActivitiesAsync()
|
||||
|
|
|
|||
|
|
@ -21,8 +21,8 @@
|
|||
</Grid.Styles>
|
||||
|
||||
<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" />
|
||||
<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,*">
|
||||
|
|
@ -76,7 +76,24 @@
|
|||
<ListBox.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:ActivityUserDisplayItem">
|
||||
<StackPanel Spacing="2" Margin="0,0,0,8">
|
||||
<StackPanel Orientation="Horizontal" Spacing="4">
|
||||
<StackPanel Orientation="Horizontal" Spacing="6" VerticalAlignment="Center">
|
||||
<Border Width="24"
|
||||
Height="24"
|
||||
CornerRadius="12"
|
||||
ClipToBounds="True"
|
||||
Background="#E9ECEF">
|
||||
<Grid>
|
||||
<TextBlock Text="{Binding AvatarFallbackLabel}"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="#495057" />
|
||||
<Image Source="{Binding AvatarImage}"
|
||||
Width="24"
|
||||
Height="24"
|
||||
Stretch="UniformToFill" />
|
||||
</Grid>
|
||||
</Border>
|
||||
<TextBlock Text="{Binding UserName}" FontWeight="Bold" />
|
||||
<Border IsVisible="{Binding HasPerformerProfile}"
|
||||
Classes="user-badge"
|
||||
|
|
|
|||
|
|
@ -24,13 +24,13 @@
|
|||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<StackPanel Grid.Row="0" Spacing="12"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Top">
|
||||
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<Button Command="{Binding RefreshAsync}" Content="Refresh" />
|
||||
<Button Command="{Binding SearchAsync}" Content="Filter" />
|
||||
<Border Grid.Row="0"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Top"
|
||||
Padding="0">
|
||||
<WrapPanel Orientation="Horizontal">
|
||||
<Button Command="{Binding RefreshAsync}" Content="🗘 Refresh" />
|
||||
<Button Command="{Binding SearchAsync}" Content="🔍 Filter" />
|
||||
<Button Command="{Binding SaveAsync}" Content="Save" />
|
||||
<Button Command="{Binding DeleteAsync}" Content="Delete" />
|
||||
<Button x:Name="ManageAclButton"
|
||||
|
|
@ -62,8 +62,8 @@
|
|||
Command="{Binding OpenSignatureDevAsync}"
|
||||
Content="[DEV] Signature"
|
||||
ToolTip.Tip="DEV ONLY — to remove when SignalR handler lands" />
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</WrapPanel>
|
||||
</Border>
|
||||
|
||||
<Border Grid.Row="1" BorderBrush="Gray" BorderThickness="1" Padding="8">
|
||||
<ListBox ItemsSource="{Binding FilteredPosts}" SelectedItem="{Binding SelectedPost, Mode=TwoWay}"
|
||||
|
|
|
|||
|
|
@ -20,14 +20,18 @@ public sealed class ActivityApiClient
|
|||
|
||||
private readonly IYavscApiClient _api;
|
||||
private readonly Uri _baseAddress;
|
||||
private readonly Uri _avatarBaseAddress;
|
||||
|
||||
public ActivityApiClient(IYavscApiClient api, string businessBaseAddress)
|
||||
public ActivityApiClient(IYavscApiClient api, string businessBaseAddress, string? avatarBaseAddress = null)
|
||||
{
|
||||
_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);
|
||||
_avatarBaseAddress = string.IsNullOrWhiteSpace(avatarBaseAddress)
|
||||
? _baseAddress
|
||||
: new Uri(avatarBaseAddress, UriKind.Absolute);
|
||||
}
|
||||
|
||||
public Task<List<ActivityInfo>> GetCatalogAsync(
|
||||
|
|
@ -59,5 +63,17 @@ public sealed class ActivityApiClient
|
|||
CancellationToken ct = default)
|
||||
=> GetUsersAsync(activityCode, ct);
|
||||
|
||||
public string BuildAvatarXsUrl(string? userName)
|
||||
{
|
||||
var siteRoot = new Uri(_avatarBaseAddress, "/");
|
||||
|
||||
if (string.IsNullOrWhiteSpace(userName))
|
||||
{
|
||||
return new Uri(siteRoot, "images/Users/icon_user.xs.png").ToString();
|
||||
}
|
||||
|
||||
return new Uri(siteRoot, $"avatars/{Uri.EscapeDataString(userName)}.xs.png").ToString();
|
||||
}
|
||||
|
||||
private string Absolute(string relativePath) => new Uri(_baseAddress, relativePath).ToString();
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue