postit: persist settings save and apply ApiUrl changes without restart #51
8 changed files with 84 additions and 30 deletions
commit
bfbcaeb400
|
|
@ -13,12 +13,14 @@
|
||||||
|
|
||||||
* [PostIt] Les avatars ne sont plus relies en string sur `Image.Source`: ils sont telecharges et lies en `Bitmap`.
|
* [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.
|
* [Yavsc.Api.Client] `ActivityApiClient` accepte une base d'avatar dediee et construit les URLs avatar depuis l'autorite d'identification.
|
||||||
|
* [PostIt] Les clients Activites/Billing utilisent maintenant `ApiUrl` en lecture dynamique: un changement via Parametres prend effet sans redemarrer l'application (apres sauvegarde et rafraichissement de la page).
|
||||||
* [PostIt] Le header de `MainPage` n'utilise plus `ScrollViewer`; remplacement par une barre de commandes basee sur `WrapPanel`.
|
* [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`.
|
* [PostIt] Alignement de la navigation blogs: renommage `PushMainPageAsync` -> `PushBlogsPageAsync` et ajustement de `HomePageViewModel`.
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
néant
|
* [PostIt.Android] Correction d'un 404 sur la page Activites au premier lancement: la configuration embarquee pointait `ApiUrl` vers le host Blogs au lieu de l'API metier.
|
||||||
|
* [PostIt] Correction du bouton Sauver de la page Parametres: binding vers `SaveCommand` pour persister correctement `ApiUrl`/`BlogsApiUrl`.
|
||||||
|
|
||||||
## [1.0.8-rc11] - unstable
|
## [1.0.8-rc11] - unstable
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,22 @@ public class ActivitiesPageViewModelTests
|
||||||
Assert.Equal("https://business.example/api/v1/billing/Rdv", api.Paths[3]);
|
Assert.Equal("https://business.example/api/v1/billing/Rdv", api.Paths[3]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ActivityApiClient_uses_updated_business_base_without_restart()
|
||||||
|
{
|
||||||
|
var api = new StubActivityApi();
|
||||||
|
var baseUrl = "https://business-a.example/api/v1/";
|
||||||
|
var client = new ActivityApiClient(api, () => baseUrl);
|
||||||
|
|
||||||
|
await client.GetCatalogAsync(ct: TestContext.Current.CancellationToken);
|
||||||
|
|
||||||
|
baseUrl = "https://business-b.example/api/v1/";
|
||||||
|
await client.GetUsersAsync("brush", TestContext.Current.CancellationToken);
|
||||||
|
|
||||||
|
Assert.Equal("https://business-a.example/api/v1/activity/catalog", api.Paths[0]);
|
||||||
|
Assert.Equal("https://business-b.example/api/v1/activity/brush/users", api.Paths[1]);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task RefreshAsync_loads_first_activity_then_specialization_performers()
|
public async Task RefreshAsync_loads_first_activity_then_specialization_performers()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -26,9 +26,9 @@ public static class ServiceCollectionHelpers
|
||||||
var userSearchClient = new UserSearchClient(api, settings.BlogsApiUrl);
|
var userSearchClient = new UserSearchClient(api, settings.BlogsApiUrl);
|
||||||
var activityClient = new ActivityApiClient(
|
var activityClient = new ActivityApiClient(
|
||||||
api,
|
api,
|
||||||
settings.ApiUrl,
|
() => settings.ApiUrl,
|
||||||
settings.Authentication?.Authority);
|
() => settings.Authentication?.Authority);
|
||||||
var billingClient = new BillingApiClient(api, settings.ApiUrl);
|
var billingClient = new BillingApiClient(api, () => settings.ApiUrl);
|
||||||
var userDirectory = new UserDirectory(userSearchClient);
|
var userDirectory = new UserDirectory(userSearchClient);
|
||||||
|
|
||||||
// Vues
|
// Vues
|
||||||
|
|
|
||||||
|
|
@ -61,16 +61,11 @@
|
||||||
Click="ChooseAvatar_Click"
|
Click="ChooseAvatar_Click"
|
||||||
Margin="0,0,8,0"/>
|
Margin="0,0,8,0"/>
|
||||||
|
|
||||||
<!-- Sauver: bound to the Save RelayCommand on the Settings
|
<!-- Sauver: bind to the RelayCommand-generated property
|
||||||
VM. The source generator emits an ICommand property whose
|
(SaveCommand). IsEnabled tracks IsDirty so the button
|
||||||
name matches the source method exactly (no "Command"
|
auto-disables when there's nothing to persist. -->
|
||||||
suffix is added), so we bind {Binding Save} here. See
|
|
||||||
AGENTS.md "Avalonia + CommunityToolkit.Mvvm : conventions
|
|
||||||
de binding pour [RelayCommand]" for the full rationale.
|
|
||||||
IsEnabled tracks IsDirty so the button auto-disables
|
|
||||||
when there's nothing to persist. -->
|
|
||||||
<Button Content="Sauver"
|
<Button Content="Sauver"
|
||||||
Command="{Binding Save}"
|
Command="{Binding SaveCommand}"
|
||||||
IsEnabled="{Binding IsDirty}"/>
|
IsEnabled="{Binding IsDirty}"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@
|
||||||
},
|
},
|
||||||
"RedirectUri": "postit://callback",
|
"RedirectUri": "postit://callback",
|
||||||
"DarkMode": false,
|
"DarkMode": false,
|
||||||
"ApiUrl": "https://blogs.pschneider.fr/api/v1/",
|
"ApiUrl": "https://api.pschneider.fr/api/v1/",
|
||||||
"Scopes": [
|
"Scopes": [
|
||||||
"openid",
|
"openid",
|
||||||
"profile",
|
"profile",
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@
|
||||||
},
|
},
|
||||||
"RedirectUri": "postit://callback",
|
"RedirectUri": "postit://callback",
|
||||||
"DarkMode": true,
|
"DarkMode": true,
|
||||||
"ApiUrl": "https://blogs.pschneider.fr/api/v1/",
|
"ApiUrl": "https://api.pschneider.fr/api/v1/",
|
||||||
"Scopes": [
|
"Scopes": [
|
||||||
"openid",
|
"openid",
|
||||||
"profile",
|
"profile",
|
||||||
|
|
|
||||||
|
|
@ -19,19 +19,29 @@ public sealed class ActivityApiClient
|
||||||
private const string PathPrefix = "activity";
|
private const string PathPrefix = "activity";
|
||||||
|
|
||||||
private readonly IYavscApiClient _api;
|
private readonly IYavscApiClient _api;
|
||||||
private readonly Uri _baseAddress;
|
private readonly Func<string> _businessBaseAddress;
|
||||||
private readonly Uri _avatarBaseAddress;
|
private readonly Func<string?> _avatarBaseAddress;
|
||||||
|
|
||||||
public ActivityApiClient(IYavscApiClient api, string businessBaseAddress, string? avatarBaseAddress = null)
|
public ActivityApiClient(IYavscApiClient api, string businessBaseAddress, string? avatarBaseAddress = null)
|
||||||
|
: this(
|
||||||
|
api,
|
||||||
|
() => businessBaseAddress,
|
||||||
|
() => avatarBaseAddress)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public ActivityApiClient(
|
||||||
|
IYavscApiClient api,
|
||||||
|
Func<string> businessBaseAddress,
|
||||||
|
Func<string?>? avatarBaseAddress = null)
|
||||||
{
|
{
|
||||||
_api = api ?? throw new ArgumentNullException(nameof(api));
|
_api = api ?? throw new ArgumentNullException(nameof(api));
|
||||||
if (string.IsNullOrEmpty(businessBaseAddress))
|
_businessBaseAddress = businessBaseAddress ?? throw new ArgumentNullException(nameof(businessBaseAddress));
|
||||||
throw new ArgumentException("Base address is required.", nameof(businessBaseAddress));
|
_avatarBaseAddress = avatarBaseAddress ?? (() => null);
|
||||||
|
|
||||||
_baseAddress = new Uri(businessBaseAddress, UriKind.Absolute);
|
// Validate initial values early to fail fast on invalid setup.
|
||||||
_avatarBaseAddress = string.IsNullOrWhiteSpace(avatarBaseAddress)
|
_ = ResolveBusinessBaseAddress();
|
||||||
? _baseAddress
|
_ = ResolveAvatarBaseAddress();
|
||||||
: new Uri(avatarBaseAddress, UriKind.Absolute);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task<List<ActivityInfo>> GetCatalogAsync(
|
public Task<List<ActivityInfo>> GetCatalogAsync(
|
||||||
|
|
@ -65,7 +75,7 @@ public sealed class ActivityApiClient
|
||||||
|
|
||||||
public string BuildAvatarXsUrl(string? userName)
|
public string BuildAvatarXsUrl(string? userName)
|
||||||
{
|
{
|
||||||
var siteRoot = new Uri(_avatarBaseAddress, "/");
|
var siteRoot = new Uri(ResolveAvatarBaseAddress(), "/");
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(userName))
|
if (string.IsNullOrWhiteSpace(userName))
|
||||||
{
|
{
|
||||||
|
|
@ -75,5 +85,22 @@ public sealed class ActivityApiClient
|
||||||
return new Uri(siteRoot, $"avatars/{Uri.EscapeDataString(userName)}.xs.png").ToString();
|
return new Uri(siteRoot, $"avatars/{Uri.EscapeDataString(userName)}.xs.png").ToString();
|
||||||
}
|
}
|
||||||
|
|
||||||
private string Absolute(string relativePath) => new Uri(_baseAddress, relativePath).ToString();
|
private string Absolute(string relativePath) => new Uri(ResolveBusinessBaseAddress(), relativePath).ToString();
|
||||||
|
|
||||||
|
private Uri ResolveBusinessBaseAddress()
|
||||||
|
{
|
||||||
|
var raw = _businessBaseAddress();
|
||||||
|
if (string.IsNullOrWhiteSpace(raw))
|
||||||
|
throw new InvalidOperationException("Business base address is required.");
|
||||||
|
|
||||||
|
return new Uri(raw, UriKind.Absolute);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Uri ResolveAvatarBaseAddress()
|
||||||
|
{
|
||||||
|
var raw = _avatarBaseAddress();
|
||||||
|
return string.IsNullOrWhiteSpace(raw)
|
||||||
|
? ResolveBusinessBaseAddress()
|
||||||
|
: new Uri(raw, UriKind.Absolute);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,15 +20,20 @@ public sealed class BillingApiClient
|
||||||
private const string PathPrefix = "billing";
|
private const string PathPrefix = "billing";
|
||||||
|
|
||||||
private readonly IYavscApiClient _api;
|
private readonly IYavscApiClient _api;
|
||||||
private readonly Uri _baseAddress;
|
private readonly Func<string> _businessBaseAddress;
|
||||||
|
|
||||||
public BillingApiClient(IYavscApiClient api, string businessBaseAddress)
|
public BillingApiClient(IYavscApiClient api, string businessBaseAddress)
|
||||||
|
: this(api, () => businessBaseAddress)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public BillingApiClient(IYavscApiClient api, Func<string> businessBaseAddress)
|
||||||
{
|
{
|
||||||
_api = api ?? throw new ArgumentNullException(nameof(api));
|
_api = api ?? throw new ArgumentNullException(nameof(api));
|
||||||
if (string.IsNullOrWhiteSpace(businessBaseAddress))
|
_businessBaseAddress = businessBaseAddress ?? throw new ArgumentNullException(nameof(businessBaseAddress));
|
||||||
throw new ArgumentException("Base address is required.", nameof(businessBaseAddress));
|
|
||||||
|
|
||||||
_baseAddress = new Uri(businessBaseAddress, UriKind.Absolute);
|
// Validate initial value early to fail fast on invalid setup.
|
||||||
|
_ = ResolveBusinessBaseAddress();
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task CreateAsync(string billingCode, object payload, CancellationToken ct = default)
|
public Task CreateAsync(string billingCode, object payload, CancellationToken ct = default)
|
||||||
|
|
@ -121,7 +126,16 @@ public sealed class BillingApiClient
|
||||||
ct: ct);
|
ct: ct);
|
||||||
}
|
}
|
||||||
|
|
||||||
private string Absolute(string relativePath) => new Uri(_baseAddress, relativePath).ToString();
|
private string Absolute(string relativePath) => new Uri(ResolveBusinessBaseAddress(), relativePath).ToString();
|
||||||
|
|
||||||
|
private Uri ResolveBusinessBaseAddress()
|
||||||
|
{
|
||||||
|
var raw = _businessBaseAddress();
|
||||||
|
if (string.IsNullOrWhiteSpace(raw))
|
||||||
|
throw new InvalidOperationException("Business base address is required.");
|
||||||
|
|
||||||
|
return new Uri(raw, UriKind.Absolute);
|
||||||
|
}
|
||||||
|
|
||||||
private static BillingQueryDetailsDto MapRdv(RdvQueryResponse dto, string billingCode)
|
private static BillingQueryDetailsDto MapRdv(RdvQueryResponse dto, string billingCode)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue