diff --git a/.forgejo/workflows/buildAndTest.yml b/.forgejo/workflows/buildAndTest.yml index f3c928ee0..b9e6f17b2 100644 --- a/.forgejo/workflows/buildAndTest.yml +++ b/.forgejo/workflows/buildAndTest.yml @@ -19,21 +19,21 @@ on: required: false type: boolean push: - branches: [ "main", "release/*", "hotfix/*", "fix/*" ] + branches: [ "main" ] pull_request: - branches: [ "main", "release/*", "hotfix/*", "fix/*" ] + branches: [ "main", "release/*" ] jobs: build: runs-on: docker container: - image: pazof/yavsc-build-env:debian13-dotnet10-android36-jdk21-v2 + image: pazof/yavsc-build-env:debian13-dotnet10-android36-jdk21-v1 steps: - name: Clone yavsc run: | set -e cd /src - git clone "${FORGEJO_SERVER_URL}/${FORGEJO_REPOSITORY}.git" _src + git clone https://forgejo.pschneider.fr/notazof/yavsc.git _src cd _src if [ -n "${GITHUB_REF:-}" ]; then git fetch origin "$GITHUB_REF" @@ -49,6 +49,12 @@ jobs: run: | echo "🏗️ Building the solution..." cd /src/_src && dotnet build --verbosity normal + - name: Install SkiaSharp native dependencies + run: | + echo "📦 Installing libfontconfig/libfreetype for SkiaSharp (Avalonia.Headless)..." + apt-get update && apt-get install -y --no-install-recommends \ + libfontconfig1 libfreetype6 libexpat1 zlib1g libbz2-1.0 libpng16-16 libbrotli1 \ + && rm -rf /var/lib/apt/lists/* - name: Test run: | echo "🚀 Lancement des tests..." diff --git a/.forgejo/workflows/release.yml b/.forgejo/workflows/release.yml index 7d0670a17..911fc831d 100644 --- a/.forgejo/workflows/release.yml +++ b/.forgejo/workflows/release.yml @@ -52,7 +52,7 @@ jobs: release: runs-on: docker container: - image: pazof/yavsc-build-env:debian13-dotnet10-android36-jdk21-v2 + image: pazof/yavsc-build-env:debian13-dotnet10-android36-jdk21-v1 steps: - name: Clone du repo au tag demandé env: @@ -69,7 +69,7 @@ jobs: cd /src if [[ ! -d _src/.git ]]; then - git clone --depth=1 "${FORGEJO_SERVER_URL}/${FORGEJO_REPOSITORY}.git" _src + git clone --depth=1 https://forgejo.pschneider.fr/notazof/yavsc.git _src fi cd _src @@ -175,6 +175,12 @@ jobs: run: | cd /src/_src dotnet restore + - name: Test + run: | + cd /src/_src && dotnet test \ + --verbosity normal \ + --filter="Category!=Platform-Android" \ + --logger "xunit;LogFileName=test-results.xml" - name: Build de PostIt.Android ARM64 run: | diff --git a/CHANGELOG.md b/CHANGELOG.md index 3f8253c70..111c1491a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,45 +1,5 @@ # Changelog -## [1.0.8-rc15] - unstable - -### Added - -* [PostIt] Saisie et envoi de pieces jointes sur un billet de blog: le client monte les fichiers en `multipart/form-data` lors du `Save`, ajoute un lien Markdown par piece jointe a l'article, puis re-PUT le billet. Une collection `DraftAttachments` porte les fichiers en cours d'edition. -* [PostIt] Apercu Markdown dans l'editeur de blogs (integration `MarkdownViewer.Core`) avec bascule edition/preview sur `BlogsPage`. -* [PostIt] Gestion des devis: page de liste `EstimateListPage` (vues client et prestataire), page d'edition `EstimateEditionPage` avec lignes de devis (`EstimateLineItemViewModel`), et flux de validation d'un devis. -* [PostIt] Flux des requetes en cours cote prestataire (`ProviderOngoingRequestsPage`) avec tri persistant. -* [Yavsc.Api] Endpoints devis `GET /api/v1/estimate/asclient` et `GET /api/v1/estimate/asprovider` pour lister les devis en cours selon le role. -* [Yavsc.Api] Endpoint `GET /api/v1/billing/provider/ongoing` et `POST /api/v1/billing/prosign/{billingCode}/{id}` pour le flux prestataire. -* [Yavsc.Api.Client] `EstimateApiClient` + DTOs (`EstimateDto`, `EstimateLineDto`) pour le pipeline devis cote client lourd. -* [Yavsc.Api.Client] Reprise de l'upload de fichiers blog dans `BlogApiClient` (`CreateMultipartContent`, `BlogUploadFile`) — la meme forme filaire que PostIt utilise en production. -* [Yavsc.Abstract] `FileServerUrlHelpers` pour deriver les URLs publiques des fichiers utilisateur depuis l'autorite OIDC (alignement sur `UserFilesPath`). -* [Yavsc.Server] `BlogSpotService.AttachFiles` : ecrit les pieces jointes sous la racine des fichiers utilisateur (`{UserFilesDirName}/{user}/blogs/{postId}/{fileName}`). -* [Yavsc.Blogs] `BlogApiController` accepte `multipart/form-data` (champ `blog` + parts `file`) sur `POST` et `PUT /api/v1/blogspot`, en plus du payload JSON existant. -* [PostIt] Variable d'environnement `POSTIT_SETTINGS_JSON` pour surcharger les parametres au lancement (utile en CI/test). -* [Yavsc.Server] Migration EF `fileACL`: `CircleAuthorizationToFile` gagne `OwnerId` + `Access` et un graphe proprieetaire, base du controle d'acces fichier par cercle. -* [Yavsc.Server] Migration EF `genericEstimate`: `Estimate.Query` passe de `RdvQuery` a `NominativeServiceCommand?` pour supporter des devis non lies a un RDV. -* [Yavsc.Server] Migrations EF `NominativeServiceCommand` et `AddHairCutQueryLocationId` (localisation de la coupe). -* [CI] Integration du scan de secrets Gitleaks dans le pipeline Forgejo + `.gitleaksignore`. -* [Tests] Non-regressions: round-trip multipart blog (`BlogApiTests`, `CreateMultipartContentTests` forme + serveur), `BlogAttachmentLinkTests`, `FileServerUrlHelpersTests`, `EstimateApiControllerTests`, VMs `EstimateList`/`EstimateEdition`/`ProviderOngoingRequests`, `BillingControllerTests` (filtrage par utilisateur), `GetOpenIdConfiguration` contre l'hote de test. - -### Changed - -* [PostIt] Renommage `MainPage` -> `BlogsPage` (et `PushMainPageAsync` -> `PushBlogsPageAsync`) pour refletter le role de la page. -* [PostIt] Refacto des `SiteSettings` : reorganisation des parametres, nouvelles valeurs par defaut, et indications plus claires dans les `appsettings`. -* [Yavsc.Api] `BillingController` ne liste plus que les codes billing de l'utilisateur authentifie (filtre par `UserId`), au lieu de l'ensemble des requetes prestataire. -* [Yavsc.Api] Mutualisation d'un `YavscMessageSender` partage dans l'hote API (au lieu d'une instance par controller). -* [Yavsc.Org] La detection du doublon d'email a l'inscription utilise desormais le code erreur generic `DbException` 23505 (fournisseur-agnostique) au lieu de `PostgresException`, pour rester correct en test SQLite. -* [PostIt] `YavscApiClient` tolere les corps vides (`204 No Content` sur `PUT /blogspot/{id}`) au lieu de lever un `JsonException`. -* [CI] Le pipeline compile avant les tests, installe les dependances natives, et retire le `ItemGroup.Using Include=Xunit` superflu. - -### Fixed - -* [PostIt] Suppression d'un faux message d'erreur affiche apres un `PUT /blogspot/{id}` reussi (204 traite comme succes, plus comme une reponse vide invalide). -* [PostIt] Correction de la creation des liens de pieces jointes (`TryAppendAttachmentLinks`) : segment proprietaire et chemin `blogs/{id}/{fileName}` alignes sur ce que `AttachFiles` ecrit reellement. -* [Yavsc.Blogs] Correction de l'upload / de la persistance des pieces jointes (`FileSystemHelpers` + `BlogApiController` multipart). -* [Tests] Stabilisation des suites: seeds billing avec metadonnees d'audit, `GetOpenIdConfiguration_returns_ok` contre l'hote de test, retraits d'assertions abusives, tests SQLite verts. -* [PostIt] Nettoyage de fichiers temporaires accidentels et ignore des repertoires `tmp` dans le depot. - ## [1.0.8-rc14] - unstable ### Added diff --git a/Directory.Build.props b/Directory.Build.props index bf1bf8bcb..83d21579d 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -2,16 +2,5 @@ Yavsc NU1701, NU1901, NU1902, NU1507 - - false diff --git a/Directory.Packages.props b/Directory.Packages.props index feea66ec9..f1be93f91 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -4,19 +4,22 @@ 8.1.0-pazofrc007 - - + + + + + @@ -34,9 +37,13 @@ + + + + @@ -46,4 +53,4 @@ - \ No newline at end of file + diff --git a/src/PostIt/Directory.Packages.props b/src/PostIt/Directory.Packages.props index e8804a8bf..2f27340e9 100644 --- a/src/PostIt/Directory.Packages.props +++ b/src/PostIt/Directory.Packages.props @@ -1,31 +1,35 @@ - + - - true - 12.1.1 - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file + + true + 12.1.1 + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/PostIt/PostIt.Android/PostIt.Android.csproj b/src/PostIt/PostIt.Android/PostIt.Android.csproj index a6d04cb33..3d8be385c 100644 --- a/src/PostIt/PostIt.Android/PostIt.Android.csproj +++ b/src/PostIt/PostIt.Android/PostIt.Android.csproj @@ -11,8 +11,8 @@ false 1.1.0.0 1.1.0.0 - 1.1.0-feat-sign-estimate.1+306.Branch.feat-sign-estimate.Sha.e9a33695b38154859729b61d1280e596f1597acf - 1.1.0-feat-sign-estimate.1 + 1.1.0-beta.1+183.Branch.release-1.0.8-rc8.Sha.6cff3db32ecf72c0d2d430b7002fa7816a34e070 + 1.1.0-beta.1 diff --git a/src/PostIt/PostIt.Browser/PostIt.Browser.csproj b/src/PostIt/PostIt.Browser/PostIt.Browser.csproj index ff2f3f2b8..4534a2940 100644 --- a/src/PostIt/PostIt.Browser/PostIt.Browser.csproj +++ b/src/PostIt/PostIt.Browser/PostIt.Browser.csproj @@ -6,8 +6,8 @@ enable 1.1.0.0 1.1.0.0 - 1.1.0-feat-sign-estimate.1+306.Branch.feat-sign-estimate.Sha.e9a33695b38154859729b61d1280e596f1597acf - 1.1.0-feat-sign-estimate.1 + 1.1.0-beta.1+183.Branch.release-1.0.8-rc8.Sha.6cff3db32ecf72c0d2d430b7002fa7816a34e070 + 1.1.0-beta.1 diff --git a/src/PostIt/PostIt.Desktop/PostIt.Desktop.csproj b/src/PostIt/PostIt.Desktop/PostIt.Desktop.csproj index 36ed1251e..948e726c5 100644 --- a/src/PostIt/PostIt.Desktop/PostIt.Desktop.csproj +++ b/src/PostIt/PostIt.Desktop/PostIt.Desktop.csproj @@ -7,8 +7,8 @@ enable 1.1.0.0 1.1.0.0 - 1.1.0-feat-sign-estimate.1+306.Branch.feat-sign-estimate.Sha.e9a33695b38154859729b61d1280e596f1597acf - 1.1.0-feat-sign-estimate.1 + 1.1.0-beta.1+183.Branch.release-1.0.8-rc8.Sha.6cff3db32ecf72c0d2d430b7002fa7816a34e070 + 1.1.0-beta.1 app.manifest diff --git a/src/PostIt/PostIt.Tests/BlogApiTestFakes.cs b/src/PostIt/PostIt.Tests/BlogApiTestFakes.cs index 112c11752..4f102be20 100644 --- a/src/PostIt/PostIt.Tests/BlogApiTestFakes.cs +++ b/src/PostIt/PostIt.Tests/BlogApiTestFakes.cs @@ -62,17 +62,4 @@ internal sealed class RecordingYavscApiClient : YavscApiClient }); return Task.FromResult(default(T)!); } - - /// Multipart overload: BlogApiClient routes - /// Create/UpdatePostAsync with attachments through here. - /// Without this override the call would fall through to the - /// base and attempt a real HTTP - /// round-trip against the stub authority. The body is a - /// factory, so we record the delegate itself; tests that - /// care about the multipart shape invoke it. - public override Task CallAsync(HttpMethod method, string path, Func contentFactory, CancellationToken ct = default) - { - _recorder.Calls.Add((method, path, contentFactory)); - return Task.FromResult(default(T)!); - } } diff --git a/src/PostIt/PostIt.Tests/BlogAttachmentLinkTests.cs b/src/PostIt/PostIt.Tests/BlogAttachmentLinkTests.cs deleted file mode 100644 index 946d3d28e..000000000 --- a/src/PostIt/PostIt.Tests/BlogAttachmentLinkTests.cs +++ /dev/null @@ -1,79 +0,0 @@ -using Yavsc.Blogspot; -using Yavsc.Api.Client; -using PostIt.ViewModels; - -namespace PostIt.Tests; - -/// -/// Pins the markdown link that -/// appends to the article after a successful attachment upload. -/// -/// The link must be an absolute URL derived from the OIDC -/// authority via FileServerUrlHelpers.GetUserFilesUri -/// (Yavsc.Abstract): the /files file server is hosted by -/// Yavsc.Org — the IdP — not by the blogs API host, so a link -/// built from BlogsApiUrl (or left relative) 404s when the -/// reader opens the post. The regression this guards: a generated -/// link of the shape /files/{user}/blogs/{id}/{name} -/// (relative, authority-less) instead of -/// https://{authority}/files/{user}/blogs/{id}/{name}. -/// -public class BlogAttachmentLinkTests -{ - [Fact] - public async Task Save_with_attachment_appends_absolute_link_derived_from_the_oidc_authority() - { - const string authority = "https://idp.example.test"; - - var recorder = new CallRecorder(); - var api = new RecordingYavscApiClient(recorder); - var blog = new BlogApiClient(api, "https://blogs.example.test/api/v1/"); - var settings = new Settings - { - Authentication = new AuthenticationSettings - { - Authority = authority, - ClientId = "stub", - Scopes = new[] { "openid" }, - }, - }; - var vm = new BlogsViewModel(blog, settings); - - // Update branch: an existing post is selected, the user - // edits the buffers and attaches a file. - vm.SelectedPost = new BlogPostDto - { - Id = 42, - AuthorId = "tester", - Title = "Avant", - Article = "Contenu initial.", - DateCreated = DateTime.UtcNow, - DateModified = DateTime.UtcNow, - }; - vm.DraftTitle = "Après"; - vm.DraftArticle = "Contenu modifié."; - vm.DraftAttachments.Add(new BlogUploadFile( - "note.txt", - System.Text.Encoding.UTF8.GetBytes("payload test"), - "text/plain")); - - await vm.SaveAsync(); - - // The update branch issues two PUTs: the multipart one - // carrying the file, then — because TryAppendAttachmentLinks - // appended a link — a JSON one carrying the final article. - var linkPut = recorder.Calls.LastOrDefault( - c => c.method == HttpMethod.Put); - Assert.NotEqual(default, linkPut); - var sent = Assert.IsType>(linkPut.body); - var multipartContent = Assert.IsType(sent()); - - var expectedUrl = $"{authority}/files/tester/blogs/42/note.txt"; - var sentStringContent = multipartContent.First() as StringContent ; - - var sentString = await sentStringContent.ReadAsStringAsync(TestContext.Current.CancellationToken); - Assert.Contains($"- [note.txt]({expectedUrl})", sentString); - // The regression shape: a relative, authority-less link. - Assert.DoesNotContain("](/files/", sentString); - } -} diff --git a/src/PostIt/PostIt.Tests/EstimateListPageViewModelTests.cs b/src/PostIt/PostIt.Tests/EstimateListPageViewModelTests.cs deleted file mode 100644 index 6158b4132..000000000 --- a/src/PostIt/PostIt.Tests/EstimateListPageViewModelTests.cs +++ /dev/null @@ -1,134 +0,0 @@ -using System.Net.Http; -using PostIt.ViewModels; -using Yavsc.Api.Client; - -namespace PostIt.Tests; - -public class EstimateListPageViewModelTests -{ - [Fact] - public async Task Client_perspective_calls_asclient_endpoint() - { - var api = new StubEstimateListApi(); - var client = new EstimateApiClient(api, "https://business.example/api/v1/"); - var vm = new EstimateListPageViewModel(client, EstimateListPerspective.Client); - - await vm.InitializeAsync(); - - Assert.Contains("https://business.example/api/v1/estimate/asclient", api.Paths); - Assert.Equal("Mes devis à valider", vm.Title); - Assert.Equal(2, vm.Estimates.Count); - // Most recent first - Assert.Equal(2, vm.Estimates[0].Id); - Assert.Equal(1, vm.Estimates[1].Id); - } - - [Fact] - public async Task Provider_perspective_calls_asprovider_endpoint() - { - var api = new StubEstimateListApi(); - var client = new EstimateApiClient(api, "https://business.example/api/v1/"); - var vm = new EstimateListPageViewModel(client, EstimateListPerspective.Provider); - - await vm.InitializeAsync(); - - Assert.Contains("https://business.example/api/v1/estimate/asprovider", api.Paths); - Assert.Equal("Mes devis en attente", vm.Title); - Assert.Equal(2, vm.Estimates.Count); - } - - [Fact] - public async Task FilterText_filters_by_title_and_counterpart() - { - var api = new StubEstimateListApi(); - var client = new EstimateApiClient(api, "https://business.example/api/v1/"); - var vm = new EstimateListPageViewModel(client, EstimateListPerspective.Provider); - - await vm.InitializeAsync(); - - vm.FilterText = "plomberie"; - Assert.Single(vm.Estimates); - Assert.Equal(1, vm.Estimates[0].Id); - - vm.FilterText = "BOB"; - Assert.Single(vm.Estimates); - Assert.Equal("bob", vm.Estimates[0].ClientId); - - vm.FilterText = string.Empty; - Assert.Equal(2, vm.Estimates.Count); - } - - [Fact] - public async Task Empty_result_sets_an_info_status() - { - var api = new StubEstimateListApi { Empty = true }; - var client = new EstimateApiClient(api, "https://business.example/api/v1/"); - var vm = new EstimateListPageViewModel(client, EstimateListPerspective.Client); - - await vm.InitializeAsync(); - - Assert.Empty(vm.Estimates); - Assert.Contains("Aucun devis", vm.StatusMessage); - } - - private sealed class StubEstimateListApi : IYavscApiClient - { - public HttpClient Http { get; } = new(); - public List Paths { get; } = new(); - public bool Empty { get; init; } - - public Task CallAsync(HttpMethod method, string path, object? body = null, CancellationToken ct = default) - { - Paths.Add(path); - - if (typeof(T) == typeof(List)) - { - var items = Empty - ? new List() - : new List - { - new() - { - Id = 1, - Title = "Devis plomberie", - Description = "Réparation fuite", - ClientId = "bob", - OwnerId = "alice", - CommandType = "Rdv", - Bill = new List - { - new() { Name = "Main d'œuvre", Description = "Intervention", Count = 1, UnitaryCost = 80m }, - }, - }, - new() - { - Id = 2, - Title = "Devis électricité", - Description = "Mise aux normes", - ClientId = "carol", - OwnerId = "alice", - CommandType = "Rdv", - Bill = new List(), - }, - }; - return Task.FromResult((T)(object)items); - } - - 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 Task CallAsync(HttpMethod method, string path, Func contentFactory, CancellationToken ct = default) - => CallAsync(method, path, (object?)null, ct); - - public Task CallAsync(HttpMethod method, string path, Func contentFactory, CancellationToken ct = default) - => CallAsync(method, path, (object?)null, ct); - - public ValueTask DisposeAsync() => ValueTask.CompletedTask; - } -} diff --git a/src/PostIt/PostIt.Tests/PostIt.Tests.csproj b/src/PostIt/PostIt.Tests/PostIt.Tests.csproj index bbe2096e7..2c4f954d9 100644 --- a/src/PostIt/PostIt.Tests/PostIt.Tests.csproj +++ b/src/PostIt/PostIt.Tests/PostIt.Tests.csproj @@ -8,8 +8,8 @@ true 1.1.0.0 1.1.0.0 - 1.1.0-feat-sign-estimate.1+306.Branch.feat-sign-estimate.Sha.e9a33695b38154859729b61d1280e596f1597acf - 1.1.0-feat-sign-estimate.1 + 1.1.0-beta.1+183.Branch.release-1.0.8-rc8.Sha.6cff3db32ecf72c0d2d430b7002fa7816a34e070 + 1.1.0-beta.1 diff --git a/src/PostIt/PostIt/Helpers/ServiceCollectionHelpers.cs b/src/PostIt/PostIt/Helpers/ServiceCollectionHelpers.cs index bc5e7298f..6bd54ca20 100644 --- a/src/PostIt/PostIt/Helpers/ServiceCollectionHelpers.cs +++ b/src/PostIt/PostIt/Helpers/ServiceCollectionHelpers.cs @@ -62,7 +62,6 @@ public static class ServiceCollectionHelpers services.AddTransient(); services.AddTransient(); services.AddTransient(); - services.AddTransient(); // ViewModels services.AddSingleton(settings); diff --git a/src/PostIt/PostIt/PostIt.csproj b/src/PostIt/PostIt/PostIt.csproj index a6f01cc31..09632bdf8 100644 --- a/src/PostIt/PostIt/PostIt.csproj +++ b/src/PostIt/PostIt/PostIt.csproj @@ -5,8 +5,8 @@ latest 1.1.0.0 1.1.0.0 - 1.1.0-feat-sign-estimate.1+306.Branch.feat-sign-estimate.Sha.e9a33695b38154859729b61d1280e596f1597acf - 1.1.0-feat-sign-estimate.1 + 1.1.0-beta.1+183.Branch.release-1.0.8-rc8.Sha.6cff3db32ecf72c0d2d430b7002fa7816a34e070 + 1.1.0-beta.1 @@ -33,7 +33,6 @@ - diff --git a/src/PostIt/PostIt/Services/YavscApiClient.cs b/src/PostIt/PostIt/Services/YavscApiClient.cs index 04dbce1fd..7e3187d8b 100644 --- a/src/PostIt/PostIt/Services/YavscApiClient.cs +++ b/src/PostIt/PostIt/Services/YavscApiClient.cs @@ -194,7 +194,10 @@ public class YavscApiClient : IYavscApiClient, IAsyncDisposable using var response = await SendAsync(method, path, body is null ? null : () => JsonContent.Create(body), ct).ConfigureAwait(false); await EnsureSuccessOrThrowAsync(response, ct).ConfigureAwait(false); - return await ReadJsonAsync(response, ct).ConfigureAwait(false); + var stream = await response.Content.ReadAsStreamAsync(ct).ConfigureAwait(false); + var dto = await JsonSerializer.DeserializeAsync(stream, + new JsonSerializerOptions { PropertyNameCaseInsensitive = true }, ct).ConfigureAwait(false); + return dto!; } /// @@ -212,31 +215,10 @@ public class YavscApiClient : IYavscApiClient, IAsyncDisposable using var response = await SendAsync(method, path, contentFactory, ct).ConfigureAwait(false); await EnsureSuccessOrThrowAsync(response, ct).ConfigureAwait(false); - return await ReadJsonAsync(response, ct).ConfigureAwait(false); - } - - /// - /// Deserialise the response body as , - /// tolerating an empty body. PUT /blogspot/{id} answers - /// 204 No Content: without this guard, - /// JsonSerializer.DeserializeAsync throws - /// JsonException: The input does not contain any JSON - /// tokens and the VM surfaces a successful save as an - /// error. Callers that genuinely need a payload - /// (GetPostAsync, CreatePostAsync) are unaffected: - /// their endpoints always return a body. - /// - private static async Task ReadJsonAsync(HttpResponseMessage response, CancellationToken ct) - { - if (response.StatusCode == HttpStatusCode.NoContent) - return default!; - - var payload = await response.Content.ReadAsStringAsync(ct).ConfigureAwait(false); - if (string.IsNullOrWhiteSpace(payload)) - return default!; - - return JsonSerializer.Deserialize(payload, - new JsonSerializerOptions { PropertyNameCaseInsensitive = true })!; + var stream = await response.Content.ReadAsStreamAsync(ct).ConfigureAwait(false); + var dto = await JsonSerializer.DeserializeAsync(stream, + new JsonSerializerOptions { PropertyNameCaseInsensitive = true }, ct).ConfigureAwait(false); + return dto!; } /// diff --git a/src/PostIt/PostIt/ViewLocator.cs b/src/PostIt/PostIt/ViewLocator.cs index 11797b714..6f8c5d902 100644 --- a/src/PostIt/PostIt/ViewLocator.cs +++ b/src/PostIt/PostIt/ViewLocator.cs @@ -54,7 +54,6 @@ public class ViewLocator : IDataTemplate BillingQueryDetailsPageViewModel => services.GetRequiredService(), ProviderOngoingRequestsPageViewModel => services.GetRequiredService(), EstimateEditionPageViewModel => services.GetRequiredService(), - EstimateListPageViewModel => services.GetRequiredService(), null => new TextBlock { Text = "No view for " }, _ => new TextBlock { Text = $"No view for {data.GetType().Name}" } }; diff --git a/src/PostIt/PostIt/ViewModels/Activity/EstimateListPageViewModel.cs b/src/PostIt/PostIt/ViewModels/Activity/EstimateListPageViewModel.cs deleted file mode 100644 index e5c4d355f..000000000 --- a/src/PostIt/PostIt/ViewModels/Activity/EstimateListPageViewModel.cs +++ /dev/null @@ -1,156 +0,0 @@ -using System; -using System.Collections.Generic; -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 PostIt.Helpers; -using Yavsc.Api.Client; - -namespace PostIt.ViewModels; - -/// -/// Point de vue d'une liste de devis en cours (non encore validés par -/// le client). -/// -public enum EstimateListPerspective -{ - /// « Mes devis à valider » — l'utilisateur courant est le client. - Client, - - /// « Mes devis en attente » — l'utilisateur courant est le fournisseur. - Provider, -} - -/// -/// Liste des devis en cours servie par GET api/v1/estimate/asclient -/// ou GET api/v1/estimate/asprovider selon la perspective choisie. -/// -public partial class EstimateListPageViewModel : ViewModelBase, IActionStatusViewModel -{ - private readonly EstimateApiClient _estimateClient; - private readonly EstimateListPerspective _perspective; - private List _allEstimates = new(); - - public EstimateListPerspective Perspective => _perspective; - - public string Title => _perspective == EstimateListPerspective.Client - ? "Mes devis à valider" - : "Mes devis en attente"; - - public string Subtitle => _perspective == EstimateListPerspective.Client - ? "Point de vue client" - : "Point de vue fournisseur"; - - /// - /// Libellé de la colonne « autre partie » : côté client on montre - /// le fournisseur (OwnerId), côté fournisseur on montre le client. - /// - public string CounterpartLabel => _perspective == EstimateListPerspective.Client - ? "Fournisseur" - : "Client"; - - [ObservableProperty] - public partial ObservableCollection Estimates { get; set; } = new(); - - [ObservableProperty] - public partial string FilterText { get; set; } = string.Empty; - - [ObservableProperty] - public partial EstimateDto? SelectedEstimate { get; set; } - - [ObservableProperty] - public partial bool IsBusy { get; set; } - - [ObservableProperty] - public partial string StatusMessage { get; set; } = "Chargement des devis..."; - - [ObservableProperty] - public partial StatusNotice ActionStatus { get; set; } = StatusNotice.Info("Chargement des devis..."); - - public override bool CanNavigateNext - { - get => false; - protected set { _ = value; } - } - - public override bool CanNavigatePrevious - { - get => true; - protected set { _ = value; } - } - - public EstimateListPageViewModel(EstimateApiClient estimateClient, EstimateListPerspective perspective) - { - _estimateClient = estimateClient ?? throw new ArgumentNullException(nameof(estimateClient)); - _perspective = perspective; - } - - public Task InitializeAsync() => RefreshAsync(); - - [RelayCommand] - public async Task RefreshAsync() - { - IsBusy = true; - try - { - var items = (_perspective == EstimateListPerspective.Client - ? await _estimateClient.GetOngoingEstimatesAsClientAsync().ConfigureAwait(true) - : await _estimateClient.GetOngoingEstimatesAsProviderAsync().ConfigureAwait(true)) - ?? new List(); - - _allEstimates = items - .OrderByDescending(x => x.Id) - .ToList(); - - ApplyFilter(); - this.SetInfoStatus(_allEstimates.Count == 0 - ? "Aucun devis en cours." - : $"{_allEstimates.Count} devis en cours chargé(s)."); - } - catch (HttpRequestException ex) when (ex.StatusCode is HttpStatusCode.Unauthorized or HttpStatusCode.Forbidden) - { - _allEstimates = new List(); - Estimates = new ObservableCollection(); - this.SetWarningStatus("Accès refusé (scope 'api'). Déconnectez puis reconnectez-vous."); - } - catch (Exception ex) - { - _allEstimates = new List(); - Estimates = new ObservableCollection(); - this.SetErrorStatus($"Erreur: {ex.Message}"); - } - finally - { - IsBusy = false; - } - } - - partial void OnFilterTextChanged(string value) - { - ApplyFilter(); - } - - private void ApplyFilter() - { - var query = FilterText?.Trim(); - var filtered = string.IsNullOrWhiteSpace(query) - ? _allEstimates - : _allEstimates.Where(x => - ContainsInsensitive(x.Title, query) - || ContainsInsensitive(x.Description, query) - || ContainsInsensitive(x.CommandType, query) - || ContainsInsensitive(x.ClientId, query) - || ContainsInsensitive(x.OwnerId, query)) - .ToList(); - - Estimates = new ObservableCollection(filtered); - } - - private static bool ContainsInsensitive(string? value, string query) - => !string.IsNullOrWhiteSpace(value) - && value.Contains(query, StringComparison.OrdinalIgnoreCase); -} diff --git a/src/PostIt/PostIt/ViewModels/Blogs/BlogsViewModel.cs b/src/PostIt/PostIt/ViewModels/Blogs/BlogsViewModel.cs index 726129f12..78be2e789 100644 --- a/src/PostIt/PostIt/ViewModels/Blogs/BlogsViewModel.cs +++ b/src/PostIt/PostIt/ViewModels/Blogs/BlogsViewModel.cs @@ -80,9 +80,6 @@ public partial class BlogsViewModel : ViewModelBase, IActionStatusViewModel [ObservableProperty] public partial Settings Settings { get; private set; } - [ObservableProperty] - public partial bool IsMarkdownViewerVisible { get; set; } - [RelayCommand] internal async Task RefreshAsync() { @@ -119,7 +116,7 @@ public partial class BlogsViewModel : ViewModelBase, IActionStatusViewModel // Build a fresh BlogPostDto from the editor buffer on // every Save — we no longer mutate SelectedPost in - // place. The previous behavior copied the buffer + // place. The previous behaviour copied the buffer // (which was a no-op when SelectedPost was null) // back onto the model and relied on a // [Required] violation to surface the missing @@ -163,7 +160,6 @@ public partial class BlogsViewModel : ViewModelBase, IActionStatusViewModel } else { - TryAppendAttachmentLinks(SelectedPost, attachments); var update = new BlogPostDto { Id = SelectedPost.Id, @@ -174,7 +170,24 @@ public partial class BlogsViewModel : ViewModelBase, IActionStatusViewModel DateCreated = SelectedPost.DateCreated, DateModified = DateTime.UtcNow, }; + await BlogClient!.UpdatePostAsync(SelectedPost.Id, update, attachments); + + if (TryAppendAttachmentLinks(SelectedPost, attachments)) + { + var linkUpdate = new BlogPostDto + { + Id = SelectedPost.Id, + AuthorId = SelectedPost.AuthorId, + Photo = SelectedPost.Photo, + Title = DraftTitle, + Article = DraftArticle ?? string.Empty, + DateCreated = SelectedPost.DateCreated, + DateModified = DateTime.UtcNow, + }; + await BlogClient.UpdatePostAsync(SelectedPost.Id, linkUpdate); + } + this.SetInfoStatus($"Billet {SelectedPost.Id} enregistré."); DraftAttachments.Clear(); } @@ -361,7 +374,6 @@ public partial class BlogsViewModel : ViewModelBase, IActionStatusViewModel public BlogsViewModel() { - WindowTitle = "Blog"; SettingsModel = new Settings(); Init(SettingsModel); BlogClient = null; @@ -377,7 +389,6 @@ public partial class BlogsViewModel : ViewModelBase, IActionStatusViewModel this.SetInfoStatus("Prêt."); Settings = settings ?? new Settings(); SearchText = Settings.SearchText; - IsMarkdownViewerVisible = false; WindowTitle = "PostIt"; DraftTitle = string.Empty; DraftArticle = string.Empty; @@ -424,7 +435,6 @@ public partial class BlogsViewModel : ViewModelBase, IActionStatusViewModel /// public BlogsViewModel(BlogApiClient blogClient, Settings? settings = null, IServiceProvider? services = null) { - WindowTitle = "Blog"; SettingsModel = new Settings(); BlogClient = blogClient ?? throw new ArgumentNullException(nameof(blogClient)); ; Services = services; diff --git a/src/PostIt/PostIt/ViewModels/Layout/HomePageViewModel.cs b/src/PostIt/PostIt/ViewModels/Layout/HomePageViewModel.cs index ea92f1d0a..fd2ff1091 100644 --- a/src/PostIt/PostIt/ViewModels/Layout/HomePageViewModel.cs +++ b/src/PostIt/PostIt/ViewModels/Layout/HomePageViewModel.cs @@ -32,15 +32,11 @@ public class HomePageViewModel : ViewModelBase OpenActivities = new AsyncRelayCommand(OpenActivitiesAsync); OpenProviderRequests = new AsyncRelayCommand(OpenProviderRequestsAsync); - OpenClientEstimates = new AsyncRelayCommand(() => OpenEstimateListAsync(EstimateListPerspective.Client)); - OpenProviderEstimates = new AsyncRelayCommand(() => OpenEstimateListAsync(EstimateListPerspective.Provider)); OpenBlogs = new AsyncRelayCommand(App.PushBlogsPageAsync); } public IAsyncRelayCommand OpenBlogs { get; } public IAsyncRelayCommand OpenActivities { get; } public IAsyncRelayCommand OpenProviderRequests { get; } - public IAsyncRelayCommand OpenClientEstimates { get; } - public IAsyncRelayCommand OpenProviderEstimates { get; } private async Task OpenActivitiesAsync() { @@ -76,25 +72,6 @@ public class HomePageViewModel : ViewModelBase await app.PushPageAsync(vm); } - private async Task OpenEstimateListAsync(EstimateListPerspective perspective) - { - var app = (App?)Application.Current; - if (app is null) - { - throw new InvalidOperationException("Application PostIt indisponible."); - } - - var estimateClient = app.ServiceProvider?.GetRequiredService(); - if (estimateClient is null) - { - throw new InvalidOperationException("Client devis indisponible."); - } - - var vm = new EstimateListPageViewModel(estimateClient, perspective); - await vm.InitializeAsync(); - await app.PushPageAsync(vm); - } - /// /// Avalonia designer constructor. Builds a self-contained VM /// with a freshly-constructed Settings so the XAML preview can diff --git a/src/PostIt/PostIt/ViewModels/Settings/Settings.cs b/src/PostIt/PostIt/ViewModels/Settings/Settings.cs index 1c2802e33..35d5a59fe 100644 --- a/src/PostIt/PostIt/ViewModels/Settings/Settings.cs +++ b/src/PostIt/PostIt/ViewModels/Settings/Settings.cs @@ -33,9 +33,6 @@ public partial class Settings : ViewModelBase [ObservableProperty] public partial string SearchText { get; set; } = string.Empty; - [ObservableProperty] - public partial bool IsMarkdownViewerVisible { get; set; } = false; - [ObservableProperty] public partial string ProviderOngoingRequestsSortOption { get; set; } = string.Empty; diff --git a/src/PostIt/PostIt/Views/Activity/EstimateListPage.axaml b/src/PostIt/PostIt/Views/Activity/EstimateListPage.axaml deleted file mode 100644 index d5055dd1f..000000000 --- a/src/PostIt/PostIt/Views/Activity/EstimateListPage.axaml +++ /dev/null @@ -1,71 +0,0 @@ - - - - - - - - -