diff --git a/.forgejo/workflows/buildAndTest.yml b/.forgejo/workflows/buildAndTest.yml
index 67b743fb5..ea58d2fef 100644
--- a/.forgejo/workflows/buildAndTest.yml
+++ b/.forgejo/workflows/buildAndTest.yml
@@ -19,19 +19,18 @@ 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 https://forgejo.pschneider.fr/notazof/yavsc.git _src
cd _src
@@ -41,17 +40,12 @@ jobs:
fi
git submodule update --init --recursive
echo "✅ Checked out at $(git rev-parse HEAD) on $(git branch --show-current 2>/dev/null || echo detached HEAD)"
- - name: Secret scan
- run: |
- echo "🔍 Scanning for secrets..."
- cd /src/_src && dotnet tool restore && dotnet picket git --verbose --redact --exit-code 1 --log-opts -n10 \
- - name: Build
- run: |
- echo "🏗️ Building the solution..."
- cd /src/_src && dotnet build --verbosity normal
+
- name: Test
run: |
echo "🚀 Lancement des tests..."
cd /src/_src && dotnet test \
--verbosity normal \
- --filter="Category!=Platform-Android"
+ --filter="Category!=Platform-Android" \
+ --logger "xunit;LogFileName=test-results.xml" \
+ && echo "✅ Success !" || echo "❌ Fail ($?)!"
diff --git a/.forgejo/workflows/release.yml b/.forgejo/workflows/release.yml
index 7b17bba42..a72f92bd6 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:
@@ -200,7 +200,6 @@ jobs:
RELEASE_BODY: ${{ env.RELEASE_BODY }}
IS_PRERELEASE: ${{ env.IS_PRERELEASE }}
run: |
- set -e
if [[ -z "$TAG" ]]; then
echo "::error::No tag resolved for the API call."
exit 1
diff --git a/.gitignore b/.gitignore
index 056cf69d2..b7813f604 100644
--- a/.gitignore
+++ b/.gitignore
@@ -35,7 +35,6 @@ appsettings-*.*.json
generated/
*.tmp
-tmp/
DataDir/
*.tests.trx
diff --git a/.gitleaksignore b/.gitleaksignore
deleted file mode 100644
index 056cf69d2..000000000
--- a/.gitleaksignore
+++ /dev/null
@@ -1,45 +0,0 @@
-# Exclure uniquement les dossiers de sortie de compilation
-bin/
-obj/
-src/*/bin/
-src/*/obj/
-test/*/bin/
-test/*/obj/
-
-# Toolchain front (Node / esbuild)
-node_modules/
-build/
-package-lock.json
-
-# Exclure les caches lourds
-.git/
-.vs/
-
-.env
-
-.*.env
-
-*.csproj.lscache
-data/
-appsettings.*.json
-appsettings-*.*.json
-
-# Exception: the Testing-environment override for Yavsc.Org is a tracked
-# configuration source, not a secrets file. TestWebApplicationFactory
-# (Yavsc.Org.Tests) flips ASPNETCORE_ENVIRONMENT to "Testing" so
-# AddConfiguration("org") in Program.Main loads this file as the
-# last in the chain (it is optional). It overrides the connection
-# string and SMTP section for the in-memory test host and contains
-# no production secrets.
-!src/Yavsc.Org/appsettings-org.Testing.json
-
-generated/
-*.tmp
-tmp/
-DataDir/
-
-*.tests.trx
-*.tests.html
-
-*.log
-
diff --git a/.vscode/launch.json b/.vscode/launch.json
index d5088f01d..dc8d3c68d 100644
--- a/.vscode/launch.json
+++ b/.vscode/launch.json
@@ -4,22 +4,6 @@
// Pour plus d'informations, visitez : https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
- {
- "name": "PostIt Desktop",
- "type": "dotnet",
- "request": "launch",
- "projectPath": "${workspaceFolder}/src/PostIt/PostIt.Desktop/PostIt.Desktop.csproj"
- },
- {
- "name": "PostIt Desktop local",
- "type": "coreclr",
- "request": "launch",
- "program": "${workspaceFolder}/src/PostIt/PostIt.Desktop/bin/Debug/net10.0/PostIt.Desktop.dll",
- "env": {
- "POSTIT_SETTINGS_JSON": "/home/paul/Workspace/yavsc/src/PostIt/PostIt/postit-settings.json"
- },
- "preLaunchTask": "dotnet: build-postit-desktop"
- },
{
"name": "Android Debug",
"type": "mono",
@@ -36,10 +20,10 @@
"port": 55555
},
{
- "name": "Yavsc API",
+ "name": "API",
"type": "dotnet",
"request": "launch",
- "projectPath": "${workspaceFolder}/src/Yavsc.Api/Yavsc.Api.csproj"
+ "projectPath": "${workspaceFolder}/src/Api/Api.csproj"
},
{
"name": "Yavsc Org",
@@ -53,7 +37,12 @@
"request": "launch",
"projectPath": "${workspaceFolder}/src/Yavsc.Blogs/Yavsc.Blogs.csproj"
},
-
+ {
+ "name": "PostIt Desktop",
+ "type": "dotnet",
+ "request": "launch",
+ "projectPath": "${workspaceFolder}/src/PostIt/PostIt.Desktop/PostIt.Desktop.csproj",
+ },
{
"name": "Test PostIt.Android launch (Xamarin.UITest)",
"type": "coreclr",
diff --git a/.vscode/tasks.json b/.vscode/tasks.json
index 71384cd8c..a30c9c065 100644
--- a/.vscode/tasks.json
+++ b/.vscode/tasks.json
@@ -47,10 +47,7 @@
"group": "build",
"isBuildCommand": true,
"isTestCommand": false,
- "isBackground": true,
- "options": {
- "cwd": "${workspaceFolder}"
- }
+ "isBackground": true
},
{
"label": "test blogs backend",
@@ -66,28 +63,6 @@
"isDefault": false
}
},
- {
- "label": "test api backend (npgsql)",
- "type": "process",
- "problemMatcher": "$msCompile",
- "command": "dotnet",
- "args": [
- "test",
- "Yavsc.Api.Test.csproj",
- "-v",
- "minimal"
- ],
- "options": {
- "cwd": "src/Yavsc.Api.Test",
- "env": {
- "YAVSC_API_TEST_DB_PROVIDER": "npgsql"
- }
- },
- "group": {
- "kind": "test",
- "isDefault": false
- }
- },
{
"label": "build-webapi",
"type": "process",
@@ -101,21 +76,6 @@
"kind": "build"
},
"isBackground": true
- },
- {
- "label": "dotnet: build-postit-desktop",
- "type": "process",
- "isBuildCommand": true,
- "isTestCommand": false,
- "isBackground": true,
- "command": "dotnet",
- "args": ["build", "/property:GenerateFullPaths=true"],
- "options": {
- "cwd": "src/PostIt/PostIt.Desktop"
- },
- "group": {
- "kind": "build"
- }
}
]
}
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/Makefile b/Makefile
index d99fe5e5e..08f0461d2 100644
--- a/Makefile
+++ b/Makefile
@@ -48,4 +48,70 @@ docker-build:
docker-run:
docker run -d -p 5000:5000 --name yavsc yavsc
-.PHONY: test install docker-image docker-build docker-run
+# Crée une branche release/ depuis main, met à jour les
+# `` des .csproj via dotnet-gitversion, et la
+# pousse sur origin.
+#
+# Usage : make release V=1.0.7-rc1
+#
+# Pré-requis : être sur main, working tree clean. La cible
+# vérifie les deux et refuse sinon — elle ne fait JAMAIS
+# de checkout automatique, c'est à l'opérateur de s'être
+# positionné sur la bonne branche au préalable (sinon le
+# bump pourrait partir sur une branche tierce par accident).
+#
+# Notes :
+# - Le nom de branche vient de l'argument V (ex: 1.0.7-rc1
+# donne release/1.0.7-rc1). C'est une étiquette d'intention,
+# pas la version assembly.
+# - La version dans les .csproj vient de GitVersion qui la
+# calcule depuis l'historique git (tag le plus proche +
+# nombre de commits). C'est la version assembly réelle.
+# - L'ordre (fetch → branche → bump → push) garantit qu'on
+# part d'un main synchro et qu'on ne pollue pas main avec
+# le bump (qui vit sur la branche release).
+# - Fail-fast si la branche existe déjà en local ou sur origin.
+release:
+ @if [ -z "$(V)" ]; then \
+ echo "Usage: make release V="; \
+ echo " V : version semver (ex. 1.0.7-rc1) — sert à nommer la branche."; \
+ exit 1; \
+ fi
+ @if [ -n "$$(git status --porcelain)" ]; then \
+ echo "Working tree sale, refus de créer une branche release."; \
+ git status --short; \
+ exit 1; \
+ fi
+ @BRANCH="release/$(V)"; \
+ if git show-ref --verify --quiet "refs/heads/$$BRANCH"; then \
+ echo "La branche $$BRANCH existe déjà en local."; \
+ echo " Pour la supprimer : git branch -D $$BRANCH"; \
+ exit 1; \
+ fi; \
+ if git ls-remote --exit-code --heads origin "$$BRANCH" >/dev/null 2>&1; then \
+ echo "La branche $$BRANCH existe déjà sur origin."; \
+ exit 1; \
+ fi; \
+ echo "==> Fetch + vérification synchro main"; \
+ git fetch origin main; \
+ if ! git merge-base --is-ancestor origin/main HEAD; then \
+ echo "main a avancé plus loin que HEAD. Fais :"; \
+ echo " git pull --ff-only origin main"; \
+ exit 1; \
+ fi; \
+ echo "==> Création de $$BRANCH depuis main"; \
+ git checkout -b "$$BRANCH"; \
+ echo "==> dotnet-gitversion /updateprojectfiles"; \
+ dotnet-gitversion /updateprojectfiles; \
+ echo "==> Commit du bump"; \
+ git add .; \
+ if git diff --cached --quiet; then \
+ echo "Pas de changements à committer (gitversion n'a produit aucune diff)."; \
+ else \
+ git commit -m "chore(release): bump version via gitversion for $(V)"; \
+ fi; \
+ echo "==> Push de $$BRANCH sur origin"; \
+ git push -u origin "$$BRANCH"; \
+ echo "==> Terminé. Branche $$BRANCH live sur origin."
+
+.PHONY: test release
diff --git a/contrib/Makefile b/contrib/Makefile
index 4e4ef4df6..79145668f 100644
--- a/contrib/Makefile
+++ b/contrib/Makefile
@@ -95,15 +95,6 @@ showConfig:
@echo CONFIGURATION: $(CONFIGURATION)
@echo BASEAPPDIR: $(BASEAPPDIR)
-showApiLogs:
- @sudo journalctl -u yavscApi.service -S "2 min ago" | tee yavscApi.log
-
-showOrgLogs:
- @sudo journalctl -u yavscOrg.service -S "2 min ago" | tee yavscOrg.log
-
-showBlogsLogs:
- @sudo journalctl -u yavscBlogs.service -S "2 min ago" | tee yavscBlogs.log
-
clean:
@rm -rf generated
diff --git a/contrib/README.md b/contrib/README.md
new file mode 100644
index 000000000..011561560
--- /dev/null
+++ b/contrib/README.md
@@ -0,0 +1,5 @@
+# Read me
+
+## Note aux icones
+
+㝉®🅬⛒⛑🩎🩺🞫🞮🞕🞖🞆🔴🔵🔲🖂🔧🔩🔐🔌💾💼💬💭👿👾🏷🎯🏹🌍🎎💩
diff --git a/dotnet-tools.json b/dotnet-tools.json
index 1762a89de..b0e38abda 100644
--- a/dotnet-tools.json
+++ b/dotnet-tools.json
@@ -1,13 +1,5 @@
{
"version": 1,
"isRoot": true,
- "tools": {
- "picket": {
- "version": "0.2.12",
- "commands": [
- "picket"
- ],
- "rollForward": false
- }
- }
+ "tools": {}
}
\ 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/ActivitiesPageViewModelTests.cs b/src/PostIt/PostIt.Tests/ActivitiesPageViewModelTests.cs
index 6cac39603..04cc0f314 100644
--- a/src/PostIt/PostIt.Tests/ActivitiesPageViewModelTests.cs
+++ b/src/PostIt/PostIt.Tests/ActivitiesPageViewModelTests.cs
@@ -162,12 +162,6 @@ public class ActivitiesPageViewModelTests
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/BillingCommandPageViewModelTests.cs b/src/PostIt/PostIt.Tests/BillingCommandPageViewModelTests.cs
index 34804b4cd..17fcc968c 100644
--- a/src/PostIt/PostIt.Tests/BillingCommandPageViewModelTests.cs
+++ b/src/PostIt/PostIt.Tests/BillingCommandPageViewModelTests.cs
@@ -352,12 +352,6 @@ public class BillingCommandPageViewModelTests
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/BillingQueriesPageViewModelTests.cs b/src/PostIt/PostIt.Tests/BillingQueriesPageViewModelTests.cs
index e53139351..a7f2b0844 100644
--- a/src/PostIt/PostIt.Tests/BillingQueriesPageViewModelTests.cs
+++ b/src/PostIt/PostIt.Tests/BillingQueriesPageViewModelTests.cs
@@ -45,8 +45,10 @@ public class BillingQueriesPageViewModelTests
Assert.Equal(2, vm.Queries.Count);
Assert.All(vm.Queries, q => Assert.DoesNotContain("Rejected", q.StatusLabel, StringComparison.OrdinalIgnoreCase));
Assert.Contains("lecture seule", vm.StatusMessage, StringComparison.OrdinalIgnoreCase);
+ Assert.False(vm.CanOpenDetails);
- Assert.True(vm.Queries.Count > 0);
+ vm.SelectedQuery = vm.Queries[0];
+ Assert.False(vm.OpenSelectedQueryCommand.CanExecute(null));
}
private sealed class StubBillingApi : IYavscApiClient
@@ -127,12 +129,6 @@ public class BillingQueriesPageViewModelTests
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/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/EstimateEditionPageViewModelTests.cs b/src/PostIt/PostIt.Tests/EstimateEditionPageViewModelTests.cs
deleted file mode 100644
index db70e25fa..000000000
--- a/src/PostIt/PostIt.Tests/EstimateEditionPageViewModelTests.cs
+++ /dev/null
@@ -1,270 +0,0 @@
-using System.Net.Http;
-using PostIt.ViewModels;
-using Yavsc;
-using Yavsc.Api.Client;
-
-namespace PostIt.Tests;
-
-public class EstimateEditionPageViewModelTests
-{
- private static BillingQuerySummaryDto SampleQuery() => new()
- {
- Id = 42,
- BillingCode = "Brush",
- ActivityCode = "hair",
- PerformerId = "perf-1",
- ClientId = "cli-1",
- Status = QueryStatus.InProgress,
- Description = "Coupe simple",
- EventDate = new DateTime(2026, 9, 12, 10, 0, 0, DateTimeKind.Utc),
- };
-
- private static EstimateEditionPageViewModel CreateViewModel(StubEstimateApi api, BillingQuerySummaryDto? query = null)
- {
- var client = new EstimateApiClient(api, "https://business.example/api/v1/");
- return new EstimateEditionPageViewModel(query ?? SampleQuery(), client);
- }
-
- [Fact]
- public void Constructor_prefills_description_and_adds_a_first_line()
- {
- var api = new StubEstimateApi();
- var vm = CreateViewModel(api);
-
- Assert.Equal("Coupe simple", vm.EstimateDescription);
- Assert.Single(vm.Lines);
- Assert.Same(vm.Lines[0], vm.SelectedLine);
- Assert.Contains("#42", vm.ContextLabel);
- Assert.Contains("cli-1", vm.ContextLabel);
- }
-
- [Fact]
- public void AddLine_appends_and_selects_the_new_line()
- {
- var api = new StubEstimateApi();
- var vm = CreateViewModel(api);
-
- vm.AddLineCommand.Execute(null);
-
- Assert.Equal(2, vm.Lines.Count);
- Assert.Same(vm.Lines[1], vm.SelectedLine);
- }
-
- [Fact]
- public void RemoveLine_removes_the_selected_line()
- {
- var api = new StubEstimateApi();
- var vm = CreateViewModel(api);
- var first = vm.Lines[0];
-
- vm.RemoveLineCommand.Execute(null);
-
- Assert.Empty(vm.Lines);
- Assert.Null(vm.SelectedLine);
- Assert.False(vm.RemoveLineCommand.CanExecute(null));
- Assert.DoesNotContain(first, vm.Lines);
- }
-
- [Fact]
- public void Total_sums_line_totals_and_tracks_edits()
- {
- var api = new StubEstimateApi();
- var vm = CreateViewModel(api);
-
- vm.Lines[0].Count = 2;
- vm.Lines[0].UnitaryCost = 15.5m;
-
- Assert.Equal(31m, vm.Total);
- Assert.Equal($"{31m:0.00} EUR", vm.TotalLabel);
-
- vm.AddLineCommand.Execute(null);
- vm.Lines[1].Count = 1;
- vm.Lines[1].UnitaryCost = 9m;
-
- Assert.Equal(40m, vm.Total);
- }
-
- [Fact]
- public async Task Send_without_title_warns_and_does_not_post()
- {
- var api = new StubEstimateApi();
- var vm = CreateViewModel(api);
- vm.Lines[0].Name = "Coupe";
- vm.Lines[0].Description = "Coupe simple";
- vm.Lines[0].UnitaryCost = 25m;
-
- await vm.SendCommand.ExecuteAsync(null);
-
- Assert.Null(api.LastBody);
- Assert.Equal(StatusSeverity.Warning, vm.ActionStatus.Severity);
- Assert.Contains("titre", vm.ActionStatus.Message);
- }
-
- [Fact]
- public async Task Send_without_any_line_warns_and_does_not_post()
- {
- var api = new StubEstimateApi();
- var vm = CreateViewModel(api);
- vm.EstimateTitle = "Devis coupe";
- vm.Lines.Clear();
-
- await vm.SendCommand.ExecuteAsync(null);
-
- Assert.Null(api.LastBody);
- Assert.Equal(StatusSeverity.Warning, vm.ActionStatus.Severity);
- Assert.Contains("ligne", vm.ActionStatus.Message);
- }
-
- [Fact]
- public async Task Send_with_a_blank_line_name_warns_and_does_not_post()
- {
- var api = new StubEstimateApi();
- var vm = CreateViewModel(api);
- vm.EstimateTitle = "Devis coupe";
- vm.Lines[0].Description = "Oubli du nom";
-
- await vm.SendCommand.ExecuteAsync(null);
-
- Assert.Null(api.LastBody);
- Assert.Equal(StatusSeverity.Warning, vm.ActionStatus.Severity);
- Assert.Contains("nom", vm.ActionStatus.Message);
- }
-
- [Fact]
- public async Task Send_posts_the_estimate_payload_to_the_estimate_route()
- {
- var api = new StubEstimateApi();
- var vm = CreateViewModel(api);
- vm.EstimateTitle = " Devis coupe ";
- vm.Lines[0].Name = "Coupe";
- vm.Lines[0].Description = "Coupe simple";
- vm.Lines[0].Count = 2.4m;
- vm.Lines[0].UnitaryCost = 25m;
-
- await vm.SendCommand.ExecuteAsync(null);
-
- Assert.Equal("https://business.example/api/v1/estimate", api.LastPath);
- Assert.Equal(HttpMethod.Post, api.LastMethod);
-
- var payload = Assert.IsType(api.LastBody);
- Assert.Equal(42, payload.CommandId);
- Assert.Equal("cli-1", payload.ClientId);
- Assert.Equal("Brush", payload.CommandType);
- Assert.Equal("Devis coupe", payload.Title);
- Assert.Equal("Coupe simple", payload.Description);
- Assert.Empty(payload.AttachedFiles);
- Assert.Empty(payload.AttachedGraphics);
-
- var line = Assert.Single(payload.Bill);
- Assert.Equal("Coupe", line.Name);
- Assert.Equal(2, line.Count);
- Assert.Equal(25m, line.UnitaryCost);
- Assert.Equal("EUR", line.Currency);
- }
-
- [Fact]
- public async Task Send_marks_the_page_as_sent_and_disables_resend()
- {
- var api = new StubEstimateApi();
- var vm = CreateViewModel(api);
- vm.EstimateTitle = "Devis coupe";
- vm.Lines[0].Name = "Coupe";
- vm.Lines[0].Description = "Coupe simple";
- vm.Lines[0].UnitaryCost = 25m;
-
- await vm.SendCommand.ExecuteAsync(null);
-
- Assert.True(vm.HasSent);
- Assert.False(vm.SendCommand.CanExecute(null));
- Assert.Equal("Devis envoyé", vm.SendLabel);
- Assert.Equal(StatusSeverity.Info, vm.ActionStatus.Severity);
- Assert.Contains("#7", vm.ActionStatus.Message);
- }
-
- [Fact]
- public async Task Send_surfaces_server_errors_as_error_status()
- {
- var api = new StubEstimateApi { Failure = new HttpRequestException("boom", null, System.Net.HttpStatusCode.InternalServerError) };
- var vm = CreateViewModel(api);
- vm.EstimateTitle = "Devis coupe";
- vm.Lines[0].Name = "Coupe";
- vm.Lines[0].Description = "Coupe simple";
-
- await vm.SendCommand.ExecuteAsync(null);
-
- Assert.False(vm.HasSent);
- Assert.Equal(StatusSeverity.Error, vm.ActionStatus.Severity);
- Assert.True(vm.SendCommand.CanExecute(null));
- }
-
- [Fact]
- public async Task Send_accepts_negative_amounts_for_discount_lines()
- {
- var api = new StubEstimateApi();
- var vm = CreateViewModel(api);
- vm.EstimateTitle = "Devis avec remise";
- vm.Lines[0].Name = "Coupe";
- vm.Lines[0].Description = "Coupe simple";
- vm.Lines[0].UnitaryCost = 25m;
-
- vm.AddLineCommand.Execute(null);
- vm.Lines[1].Name = "Remise fidélité";
- vm.Lines[1].Description = "Remise client régulier";
- vm.Lines[1].UnitaryCost = -5m;
-
- Assert.Equal(20m, vm.Total);
-
- await vm.SendCommand.ExecuteAsync(null);
-
- var payload = Assert.IsType(api.LastBody);
- Assert.Equal(2, payload.Bill.Count);
- Assert.Equal(-5m, payload.Bill[1].UnitaryCost);
- Assert.True(vm.HasSent);
- }
-
- private sealed class StubEstimateApi : IYavscApiClient
- {
- public HttpClient Http { get; } = new();
- public string? LastPath { get; private set; }
- public HttpMethod? LastMethod { get; private set; }
- public object? LastBody { get; private set; }
- public Exception? Failure { get; init; }
-
- public Task CallAsync(HttpMethod method, string path, object? body = null, CancellationToken ct = default)
- {
- LastMethod = method;
- LastPath = path;
- LastBody = body;
-
- if (Failure is not null)
- {
- throw Failure;
- }
-
- if (typeof(T) == typeof(EstimateCreatedDto))
- {
- var payload = (EstimateDto)body!;
- var created = new EstimateCreatedDto { Id = 7, Bill = payload.Bill };
- return Task.FromResult((T)(object)created);
- }
-
- return Task.FromResult(default(T)!);
- }
-
- public Task CallAsync(HttpMethod method, string path, object? body = null, CancellationToken ct = default)
- {
- LastMethod = method;
- LastPath = path;
- LastBody = body;
- 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/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/HomePageProviderFlowTests.cs b/src/PostIt/PostIt.Tests/HomePageProviderFlowTests.cs
deleted file mode 100644
index 16f01c5e9..000000000
--- a/src/PostIt/PostIt.Tests/HomePageProviderFlowTests.cs
+++ /dev/null
@@ -1,15 +0,0 @@
-using PostIt.ViewModels;
-
-namespace PostIt.Tests;
-
-public class HomePageProviderFlowTests
-{
- [Fact]
- public void HomePage_exposes_provider_requests_command()
- {
- var vm = new HomePageViewModel();
-
- Assert.NotNull(vm.OpenProviderRequests);
- Assert.True(vm.OpenProviderRequests.CanExecute(null));
- }
-}
diff --git a/src/PostIt/PostIt.Tests/MainPageButtonsTests.cs b/src/PostIt/PostIt.Tests/MainPageButtonsTests.cs
index f72c8d4d5..d1d00532d 100644
--- a/src/PostIt/PostIt.Tests/MainPageButtonsTests.cs
+++ b/src/PostIt/PostIt.Tests/MainPageButtonsTests.cs
@@ -8,8 +8,6 @@ using Yavsc.Blogspot;
using PostIt.Services;
using PostIt.ViewModels;
using PostIt.Views;
-using PostIt.Views.Blogs;
-using PostIt.Helpers;
namespace PostIt.Tests;
@@ -74,13 +72,13 @@ public class MainPageButtonsTests
{ }
}
- private static BlogsViewModel MakeViewModel(BlogPostDto? selectedPost = null)
+ private static MainViewModel MakeViewModel(BlogPostDto? selectedPost = null)
{
var api = new ThrowingApi();
var blog = new BlogApiClient(api, "http://localhost/");
var circle = new CircleApiClient(api, "http://localhost/");
var acl = new BlogAclApiClient(api, "http://localhost/");
- // Minimal DI graph: only what BlogsViewModel resolves
+ // Minimal DI graph: only what MainPageViewModel resolves
// when the user clicks a navigation button. Today that's
// SignaturePageViewModel / CirclesPageViewModel / ACL
// dependencies. The graph intentionally stays local to this
@@ -95,7 +93,7 @@ public class MainPageButtonsTests
services.AddTransient();
services.AddTransient();
services.AddTransient();
- var vm = new BlogsViewModel(blog, services: services.BuildServiceProvider());
+ var vm = new MainViewModel(blog, services: services.BuildServiceProvider());
if (selectedPost is not null) vm.SelectedPost = selectedPost;
return vm;
}
@@ -103,7 +101,7 @@ public class MainPageButtonsTests
///
/// Mount a real (as
/// SessionStatusBannerTests does), push a
- /// with the given VM onto
+ /// with the given VM onto
/// NavRoot. PushAsync is awaited (via
/// GetAwaiter().GetResult()) so the page is on the
/// nav stack before the test tries to interact with its
@@ -111,17 +109,13 @@ public class MainPageButtonsTests
/// realised and KeyPressQwerty has a real
/// to dispatch against.
///
- private static (MainView window, BlogsPage page) MountMainPage(BlogsViewModel vm)
+ private static (MainView window, MainPage page) MountMainPage(MainViewModel vm)
{
var window = new MainView();
- var page = new BlogsPage { DataContext = vm };
+ var page = new MainPage { DataContext = vm };
var app = (PostIt.App)Application.Current!;
-
app.AttachMainWindow(window);
-
window.NavRoot.PushAsync(page).GetAwaiter().GetResult();
- var mainWindow = new Window { Content = window };
- mainWindow.Show();
return (window, page);
}
@@ -151,7 +145,7 @@ public class MainPageButtonsTests
}
[AvaloniaFact]
- public async Task Acl_button_click_pushes_a_page_onto_nav_stack()
+ public void Acl_button_click_pushes_a_page_onto_nav_stack()
{
// Arrange: a VM whose SelectedPost is non-null so
// CanManageAcl evaluates to true and the button is
@@ -184,7 +178,7 @@ public class MainPageButtonsTests
}
[AvaloniaFact]
- public async Task Circles_button_click_pushes_a_page_onto_nav_stack()
+ public void Circles_button_click_pushes_a_page_onto_nav_stack()
{
// Arrange: OpenCircles has no CanExecute guard today —
// any click should fire it and push the page.
@@ -209,7 +203,7 @@ public class MainPageButtonsTests
public void Signature_dev_button_click_pushes_a_page_onto_nav_stack()
{
// Arrange: the "[DEV] Signature" button is bound to the
- // BlogsViewModel.OpenSignatureDevCommand [RelayCommand].
+ // MainPageViewModel.OpenSignatureDevCommand [RelayCommand].
// The click must push SignaturePage on top of NavRoot.
// The ServiceCollection registered in MakeViewModel provides
// SignaturePageViewModel so the command can resolve it via
diff --git a/src/PostIt/PostIt.Tests/MainPageSaveTests.cs b/src/PostIt/PostIt.Tests/MainPageSaveTests.cs
index 519cd141d..0fd5627c8 100644
--- a/src/PostIt/PostIt.Tests/MainPageSaveTests.cs
+++ b/src/PostIt/PostIt.Tests/MainPageSaveTests.cs
@@ -5,11 +5,10 @@ using Yavsc.Blogspot;
using Yavsc.Api.Client;
using PostIt.ViewModels;
using PostIt.Views;
-using PostIt.Views.Blogs;
namespace PostIt.Tests;
///
-/// Headless UI tests for the "Save" flow in .
+/// Headless UI tests for the "Save" flow in .
/// The pattern is the one SessionStatusBannerTests
/// established: [AvaloniaFact], a
/// hosting the page (via a because
@@ -41,9 +40,9 @@ public class MainPageSaveTests
var recorder = new CallRecorder();
var api = new RecordingYavscApiClient(recorder);
var blog = new BlogApiClient(api, "http://localhost/");
- var viewModel = new BlogsViewModel(blog);
+ var viewModel = new MainViewModel(blog);
- var page = new BlogsPage { DataContext = viewModel };
+ var page = new MainPage { DataContext = viewModel };
// MainPage is a ContentPage (a Page, not a Control), so it
// must be hosted in a navigation surface. The production
// MainWindow.axaml uses NavigationPage, and the API is the
@@ -64,7 +63,9 @@ public class MainPageSaveTests
const string typed = "Mon premier billet";
titleBox.Text = typed;
- var saveButton = page.SaveButton;
+ var saveButton = window.GetVisualDescendants()
+ .OfType
diff --git a/src/PostIt/PostIt.Tests/PostItViewModelTests.cs b/src/PostIt/PostIt.Tests/PostItViewModelTests.cs
index 8ecc5bb55..1a867bd61 100644
--- a/src/PostIt/PostIt.Tests/PostItViewModelTests.cs
+++ b/src/PostIt/PostIt.Tests/PostItViewModelTests.cs
@@ -11,23 +11,25 @@ public class PostItViewModelTests
[Fact]
public void SearchCommand_filters_posts_by_title_article_or_author()
{
- // BlogsViewModel no longer owns a BlogApiClient instance by
+ // MainPageViewModel no longer owns a BlogApiClient instance by
// default; tests construct one with a fake YavscApiClient that
// throws on any call (we never call the API in this test).
var fakeApi = new ThrowingYavscApiClient();
var blog = new BlogApiClient(fakeApi, "http://localhost/");
- var viewModel = new BlogsViewModel(blog);
+ var viewModel = new MainViewModel(blog);
viewModel.Posts.Add(new BlogPostDto { Id = 1, Title = "First post", Article = "Hello world", AuthorId = "alice" });
viewModel.Posts.Add(new BlogPostDto { Id = 2, Title = "Second post", Article = "Nothing here", AuthorId = "bob" });
viewModel.Posts.Add(new BlogPostDto { Id = 3, Title = "Third post", Article = "Search me", AuthorId = "carol" });
viewModel.SearchText = "search";
+ viewModel.SearchCommand.Execute(null);
Assert.Single(viewModel.FilteredPosts);
Assert.Equal(3, viewModel.FilteredPosts[0].Id);
viewModel.SearchText = "bob";
+ viewModel.SearchCommand.Execute(null);
Assert.Single(viewModel.FilteredPosts);
Assert.Equal(2, viewModel.FilteredPosts[0].Id);
@@ -58,7 +60,7 @@ public class PostItViewModelTests
{
var api = new RecordingPublishApi();
var blog = new BlogApiClient(api, "http://localhost/");
- var viewModel = new BlogsViewModel(blog);
+ var viewModel = new MainViewModel(blog);
viewModel.SelectedPost = new BlogPostDto { Id = 42, IsPublished = false };
@@ -144,12 +146,6 @@ public class PostItViewModelTests
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/ProviderOngoingRequestsPageViewModelTests.cs b/src/PostIt/PostIt.Tests/ProviderOngoingRequestsPageViewModelTests.cs
deleted file mode 100644
index cfb300316..000000000
--- a/src/PostIt/PostIt.Tests/ProviderOngoingRequestsPageViewModelTests.cs
+++ /dev/null
@@ -1,238 +0,0 @@
-using System.Net.Http;
-using PostIt.ViewModels;
-using Yavsc;
-using Yavsc.Api.Client;
-
-namespace PostIt.Tests;
-
-public class ProviderOngoingRequestsPageViewModelTests
-{
- [Fact]
- public async Task RefreshAsync_calls_provider_endpoint_and_filters_out_unknown_billing_codes()
- {
- var api = new StubProviderApi();
- var client = new BillingApiClient(api, "https://business.example/api/v1/");
- var vm = new ProviderOngoingRequestsPageViewModel(client);
-
- await vm.InitializeAsync();
-
- Assert.Contains("https://business.example/api/v1/bill/provider/ongoing", api.Paths);
- Assert.Equal(3, vm.Queries.Count);
- Assert.Equal(12, vm.Queries[0].Id);
- Assert.Equal(11, vm.Queries[1].Id);
- Assert.Equal(10, vm.Queries[2].Id);
- }
-
- [Fact]
- public async Task FilterText_filters_by_activity_code_and_status()
- {
- var api = new StubProviderApi();
- var client = new BillingApiClient(api, "https://business.example/api/v1/");
- var vm = new ProviderOngoingRequestsPageViewModel(client);
-
- await vm.InitializeAsync();
-
- vm.FilterText = "mbrush";
- Assert.Single(vm.Queries);
- Assert.Equal("MBrush", vm.Queries[0].BillingCode);
-
- vm.FilterText = "accepted";
- Assert.Single(vm.Queries);
- Assert.Equal(QueryStatus.Accepted, vm.Queries[0].Status);
- }
-
- [Fact]
- public async Task OpenSelectedEditorCommand_can_execute_only_when_selection_exists()
- {
- var api = new StubProviderApi();
- var client = new BillingApiClient(api, "https://business.example/api/v1/");
- var vm = new ProviderOngoingRequestsPageViewModel(client);
-
- await vm.InitializeAsync();
-
- Assert.False(vm.OpenSelectedEditorCommand.CanExecute(null));
-
- vm.SelectedQuery = vm.Queries[0];
-
- Assert.True(vm.OpenSelectedEditorCommand.CanExecute(null));
- }
-
- [Fact]
- public async Task SelectedSortOption_date_keeps_most_recent_first()
- {
- var api = new StubProviderApi();
- var client = new BillingApiClient(api, "https://business.example/api/v1/");
- var vm = new ProviderOngoingRequestsPageViewModel(client);
-
- await vm.InitializeAsync();
- vm.SelectedSortOption = ProviderOngoingRequestsPageViewModel.SortByDate;
-
- Assert.Equal(new long[] { 12, 11, 10 }, vm.Queries.Select(q => q.Id).ToArray());
- }
-
- [Fact]
- public async Task SelectedSortOption_date_ascending_keeps_oldest_first()
- {
- var api = new StubProviderApi();
- var client = new BillingApiClient(api, "https://business.example/api/v1/");
- var vm = new ProviderOngoingRequestsPageViewModel(client);
-
- await vm.InitializeAsync();
- vm.SelectedSortOption = ProviderOngoingRequestsPageViewModel.SortByDateAsc;
-
- Assert.Equal(new long[] { 10, 11, 12 }, vm.Queries.Select(q => q.Id).ToArray());
- }
-
- [Fact]
- public async Task SelectedSortOption_status_prioritizes_inprogress_then_accepted_then_inserted()
- {
- var api = new StubProviderApi();
- var client = new BillingApiClient(api, "https://business.example/api/v1/");
- var vm = new ProviderOngoingRequestsPageViewModel(client);
-
- await vm.InitializeAsync();
- vm.SelectedSortOption = ProviderOngoingRequestsPageViewModel.SortByStatus;
-
- Assert.Equal(new long[] { 11, 12, 10 }, vm.Queries.Select(q => q.Id).ToArray());
- }
-
- [Fact]
- public void Constructor_reads_saved_sort_option_from_settings()
- {
- var api = new StubProviderApi();
- var client = new BillingApiClient(api, "https://business.example/api/v1/");
- var settings = new Settings
- {
- ProviderOngoingRequestsSortOption = ProviderOngoingRequestsPageViewModel.SortByStatus,
- };
-
- var vm = new ProviderOngoingRequestsPageViewModel(client, settings);
-
- Assert.Equal(ProviderOngoingRequestsPageViewModel.SortByStatus, vm.SelectedSortOption);
- }
-
- [Fact]
- public void Constructor_falls_back_to_default_when_saved_sort_is_invalid()
- {
- var api = new StubProviderApi();
- var client = new BillingApiClient(api, "https://business.example/api/v1/");
- var settings = new Settings
- {
- ProviderOngoingRequestsSortOption = "invalide",
- };
-
- var vm = new ProviderOngoingRequestsPageViewModel(client, settings);
-
- Assert.Equal(ProviderOngoingRequestsPageViewModel.SortByDate, vm.SelectedSortOption);
- }
-
- [Fact]
- public async Task CreateEstimateForSelectedCommand_requires_a_selection()
- {
- var api = new StubProviderApi();
- var client = new BillingApiClient(api, "https://business.example/api/v1/");
- var estimateClient = new EstimateApiClient(api, "https://business.example/api/v1/");
- var vm = new ProviderOngoingRequestsPageViewModel(client, estimateClient: estimateClient);
-
- await vm.InitializeAsync();
-
- Assert.False(vm.CreateEstimateForSelectedCommand.CanExecute(null));
-
- vm.SelectedQuery = vm.Queries[0];
-
- Assert.True(vm.CreateEstimateForSelectedCommand.CanExecute(null));
- }
-
- [Fact]
- public async Task CreateEstimateForSelectedCommand_is_disabled_without_estimate_client()
- {
- var api = new StubProviderApi();
- var client = new BillingApiClient(api, "https://business.example/api/v1/");
- var vm = new ProviderOngoingRequestsPageViewModel(client);
-
- await vm.InitializeAsync();
- vm.SelectedQuery = vm.Queries[0];
-
- Assert.False(vm.CreateEstimateForSelectedCommand.CanExecute(null));
- }
-
- private sealed class StubProviderApi : IYavscApiClient
- {
- public HttpClient Http { get; } = new();
- public List Paths { get; } = new();
-
- public Task CallAsync(HttpMethod method, string path, object? body = null, CancellationToken ct = default)
- {
- Paths.Add(path);
-
- if (typeof(T) == typeof(List))
- {
- var data = new List
- {
- new()
- {
- Id = 10,
- BillingCode = "Rdv",
- ActivityCode = "dev",
- PerformerId = "perf-1",
- ClientId = "cli-1",
- Status = QueryStatus.Inserted,
- Description = "Rendez-vous",
- EventDate = new DateTime(2026, 9, 10, 10, 0, 0, DateTimeKind.Utc),
- },
- new()
- {
- Id = 11,
- BillingCode = "MBrush",
- ActivityCode = "hair",
- PerformerId = "perf-1",
- ClientId = "cli-2",
- Status = QueryStatus.InProgress,
- Description = "Coupe multiple",
- EventDate = new DateTime(2026, 9, 11, 10, 0, 0, DateTimeKind.Utc),
- },
- new()
- {
- Id = 12,
- BillingCode = "Brush",
- ActivityCode = "hair",
- PerformerId = "perf-1",
- ClientId = "cli-3",
- Status = QueryStatus.Accepted,
- Description = "Coupe simple",
- EventDate = new DateTime(2026, 9, 12, 10, 0, 0, DateTimeKind.Utc),
- },
- new()
- {
- Id = 13,
- BillingCode = "",
- ActivityCode = "unknown",
- PerformerId = "perf-1",
- ClientId = "cli-4",
- Status = QueryStatus.Accepted,
- Description = "Doit être filtrée",
- EventDate = new DateTime(2026, 9, 13, 10, 0, 0, DateTimeKind.Utc),
- }
- };
-
- return Task.FromResult((T)(object)data);
- }
-
- 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/RdvPageHeadlessTests.cs b/src/PostIt/PostIt.Tests/RdvPageHeadlessTests.cs
index 1bcb8a0d8..fdf8689ab 100644
--- a/src/PostIt/PostIt.Tests/RdvPageHeadlessTests.cs
+++ b/src/PostIt/PostIt.Tests/RdvPageHeadlessTests.cs
@@ -74,12 +74,6 @@ public class RdvPageHeadlessTests
public Task CallAsync(HttpMethod method, string path, object? body = null, CancellationToken ct = default)
=> 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;
}
-}
+}
\ No newline at end of file
diff --git a/src/PostIt/PostIt/App.axaml.cs b/src/PostIt/PostIt/App.axaml.cs
index b80351e17..59d5e412c 100644
--- a/src/PostIt/PostIt/App.axaml.cs
+++ b/src/PostIt/PostIt/App.axaml.cs
@@ -46,48 +46,35 @@ public partial class App : Application
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
{
- InitializeClassicDesktopLifetime(settings, desktop);
+ var window = ServiceProvider.GetRequiredService();
+ desktop.MainWindow = window;
+ View = window.MainView;
+ this.ConfigureRootView(window.MainView);
+
+ ApplyDarkMode(settings);
}
else if (ApplicationLifetime is IActivityApplicationLifetime singleViewFactoryApplicationLifetime)
{
- InitializeActivityLifetime(settings, singleViewFactoryApplicationLifetime);
+ singleViewFactoryApplicationLifetime.MainViewFactory =
+ () =>
+ {
+ View = ServiceProvider.GetRequiredService();
+ this.ConfigureRootView(View);
+ ApplyDarkMode(settings);
+ return View;
+ };
}
else if (ApplicationLifetime is ISingleViewApplicationLifetime singleViewPlatform)
{
- InitializeSingleViewLifetime(settings, singleViewPlatform);
+ singleViewPlatform.MainView = View = ServiceProvider.GetRequiredService();
+ ConfigureRootView(View);
+ ApplyDarkMode(settings);
}
base.OnFrameworkInitializationCompleted();
}
- public void InitializeSingleViewLifetime(Settings settings, ISingleViewApplicationLifetime singleViewPlatform)
- {
- singleViewPlatform.MainView = View = ServiceProvider!.GetRequiredService();
- ConfigureRootView(View);
- ApplyDarkMode(settings);
- }
-
- public void InitializeActivityLifetime(Settings settings, IActivityApplicationLifetime singleViewFactoryApplicationLifetime)
- {
- singleViewFactoryApplicationLifetime.MainViewFactory =
- () =>
- {
- this.ConfigureRootView(ServiceProvider!.GetRequiredService());
- ApplyDarkMode(settings);
- return View!;
- };
- }
-
- public void InitializeClassicDesktopLifetime(Settings settings, IClassicDesktopStyleApplicationLifetime desktop)
- {
- var window = ServiceProvider!.GetRequiredService();
- desktop.MainWindow = window;
- this.ConfigureRootView(window.MainView);
-
- ApplyDarkMode(settings);
- }
-
- public void ConfigureRootView(MainView rootView)
+private void ConfigureRootView(MainView rootView)
{
// Déclencher le Boot une seule fois lors du chargement du contrôle à l'écran.
rootView.AttachedToVisualTree += async (_, _) => await BootOnceAsync();
@@ -100,7 +87,6 @@ public partial class App : Application
};
rootView.SessionBanner.DataContext = sessionStatus;
- this.View = rootView;
}
private async Task BootOnceAsync()
@@ -162,7 +148,7 @@ public partial class App : Application
public static async Task PushBlogsPageAsync()
{
var app = (App)Current!;
- var mainVm = app.ServiceProvider!.GetRequiredService();
+ var mainVm = app.ServiceProvider!.GetRequiredService();
await mainVm.InitializeAsync();
await app.PushPageAsync(mainVm);
}
diff --git a/src/PostIt/PostIt/Helpers/ServiceCollectionHelpers.cs b/src/PostIt/PostIt/Helpers/ServiceCollectionHelpers.cs
index bc5e7298f..c0831b290 100644
--- a/src/PostIt/PostIt/Helpers/ServiceCollectionHelpers.cs
+++ b/src/PostIt/PostIt/Helpers/ServiceCollectionHelpers.cs
@@ -3,7 +3,6 @@ using Microsoft.Extensions.DependencyInjection;
using PostIt.Services;
using PostIt.ViewModels;
using PostIt.Views;
-using PostIt.Views.Blogs;
using PostIt.Views.Commands;
using Yavsc.Api.Client;
@@ -30,13 +29,12 @@ public static class ServiceCollectionHelpers
() => settings.ApiUrl,
() => settings.Authentication?.Authority);
var billingClient = new BillingApiClient(api, () => settings.ApiUrl);
- var estimateClient = new EstimateApiClient(api, () => settings.ApiUrl);
var userDirectory = new UserDirectory(userSearchClient);
var reverseGeocoding = new NominatimReverseGeocodingService();
// Vues
services.AddSingleton();
- services.AddSingleton();
+ services.AddSingleton();
services.AddSingleton();
// SettingsPage is a singleton: there must be one and only one
@@ -60,10 +58,6 @@ public static class ServiceCollectionHelpers
services.AddTransient();
services.AddTransient();
services.AddTransient();
- services.AddTransient();
- services.AddTransient();
- services.AddTransient();
-
// ViewModels
services.AddSingleton(settings);
services.AddSingleton(api);
@@ -73,7 +67,6 @@ public static class ServiceCollectionHelpers
services.AddSingleton(userSearchClient);
services.AddSingleton(activityClient);
services.AddSingleton(billingClient);
- services.AddSingleton(estimateClient);
services.AddSingleton(reverseGeocoding);
services.AddSingleton(userDirectory);
services.AddSingleton();
@@ -96,7 +89,7 @@ public static class ServiceCollectionHelpers
sessionStatus.Refresh();
services.AddSingleton(sessionStatus);
services.AddSingleton();
- services.AddSingleton();
+ services.AddSingleton();
return services.BuildServiceProvider();
}
}
diff --git a/src/PostIt/PostIt/Helpers/ViewModelBaseHelpers.cs b/src/PostIt/PostIt/Helpers/ViewModelBaseHelpers.cs
index f30e021d9..e4250cbb9 100644
--- a/src/PostIt/PostIt/Helpers/ViewModelBaseHelpers.cs
+++ b/src/PostIt/PostIt/Helpers/ViewModelBaseHelpers.cs
@@ -8,7 +8,7 @@ namespace PostIt.Helpers;
public static class ViewModelBaseHelpers
{
- public static async Task PushPageAsync(this App app, ViewModelBase vm)
+ public static async Task PushPageAsync(this App app, ViewModelBase vm)
{
var window = app.View;
if (window is null)
@@ -43,11 +43,9 @@ public static class ViewModelBaseHelpers
var stack = window.NavRoot.NavigationStack;
if (stack.Count > 0 && ReferenceEquals(stack[stack.Count - 1], page))
{
- return page;
+ return;
}
await window.NavRoot.PushAsync(page);
-
- return page;
}
}
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..4d748f87d 100644
--- a/src/PostIt/PostIt/Services/YavscApiClient.cs
+++ b/src/PostIt/PostIt/Services/YavscApiClient.cs
@@ -191,52 +191,13 @@ public class YavscApiClient : IYavscApiClient, IAsyncDisposable
object? body = null,
CancellationToken ct = default)
{
- using var response = await SendAsync(method, path, body is null ? null : () => JsonContent.Create(body), ct).ConfigureAwait(false);
+ using var response = await SendAsync(method, path, body, ct).ConfigureAwait(false);
await EnsureSuccessOrThrowAsync(response, ct).ConfigureAwait(false);
- return await ReadJsonAsync(response, ct).ConfigureAwait(false);
- }
-
- ///
- /// Call a multipart endpoint, transparently refreshing the token if needed.
- ///
- public virtual async Task CallAsync(
- HttpMethod method,
- string path,
- Func contentFactory,
- CancellationToken ct = default)
- {
- if (contentFactory is null)
- throw new ArgumentNullException(nameof(contentFactory));
-
- 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!;
}
///
@@ -256,23 +217,7 @@ public class YavscApiClient : IYavscApiClient, IAsyncDisposable
object? body = null,
CancellationToken ct = default)
{
- using var response = await SendAsync(method, path, body is null ? null : () => JsonContent.Create(body), ct).ConfigureAwait(false);
- await EnsureSuccessOrThrowAsync(response, ct).ConfigureAwait(false);
- }
-
- ///
- /// Call a multipart endpoint that returns no useful body (DELETE, etc.).
- ///
- public async Task CallAsync(
- HttpMethod method,
- string path,
- Func contentFactory,
- CancellationToken ct = default)
- {
- if (contentFactory is null)
- throw new ArgumentNullException(nameof(contentFactory));
-
- using var response = await SendAsync(method, path, contentFactory, ct).ConfigureAwait(false);
+ using var response = await SendAsync(method, path, body, ct).ConfigureAwait(false);
await EnsureSuccessOrThrowAsync(response, ct).ConfigureAwait(false);
}
@@ -287,7 +232,7 @@ public class YavscApiClient : IYavscApiClient, IAsyncDisposable
=> CallAsync(method, path, body: null, ct);
private async Task SendAsync(
- HttpMethod method, string path, Func? contentFactory, CancellationToken ct)
+ HttpMethod method, string path, object? body, CancellationToken ct)
{
if (_tokens is null)
throw new InvalidOperationException("Not logged in. Call LoginInteractiveAsync first.");
@@ -295,8 +240,8 @@ public class YavscApiClient : IYavscApiClient, IAsyncDisposable
await EnsureFreshTokenAsync(ct).ConfigureAwait(false);
using var req = new HttpRequestMessage(method, path);
- if (contentFactory is not null)
- req.Content = contentFactory();
+ if (body is not null)
+ req.Content = JsonContent.Create(body);
var response = await Http.SendAsync(req, ct).ConfigureAwait(false);
if (response.StatusCode == HttpStatusCode.Unauthorized)
@@ -307,8 +252,8 @@ public class YavscApiClient : IYavscApiClient, IAsyncDisposable
await ForceRefreshAsync(ct).ConfigureAwait(false);
using var retry = new HttpRequestMessage(method, path);
- if (contentFactory is not null)
- retry.Content = contentFactory();
+ if (body is not null)
+ retry.Content = JsonContent.Create(body);
response = await Http.SendAsync(retry, ct).ConfigureAwait(false);
}
diff --git a/src/PostIt/PostIt/ViewLocator.cs b/src/PostIt/PostIt/ViewLocator.cs
index 11797b714..f866e1bac 100644
--- a/src/PostIt/PostIt/ViewLocator.cs
+++ b/src/PostIt/PostIt/ViewLocator.cs
@@ -6,7 +6,6 @@ using Microsoft.Extensions.DependencyInjection;
using PostIt.ViewModels;
using PostIt.ViewModels.Commands;
using PostIt.Views;
-using PostIt.Views.Blogs;
using PostIt.Views.Commands;
namespace PostIt;
@@ -39,7 +38,7 @@ public class ViewLocator : IDataTemplate
var services = app!.ServiceProvider!;
return data switch
{
- BlogsViewModel => services.GetRequiredService(),
+ MainViewModel => services.GetRequiredService(),
Settings => services.GetRequiredService(),
HomePageViewModel => services.GetRequiredService(),
ActivitiesPageViewModel => services.GetRequiredService(),
@@ -52,9 +51,6 @@ public class ViewLocator : IDataTemplate
PostAclDialogViewModel => services.GetRequiredService(),
BillingQueriesPageViewModel => services.GetRequiredService(),
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/Layout/ActionStatusViewModelExtensions.cs b/src/PostIt/PostIt/ViewModels/ActionStatusViewModelExtensions.cs
similarity index 100%
rename from src/PostIt/PostIt/ViewModels/Layout/ActionStatusViewModelExtensions.cs
rename to src/PostIt/PostIt/ViewModels/ActionStatusViewModelExtensions.cs
diff --git a/src/PostIt/PostIt/ViewModels/Activity/ActivitiesPageViewModel.cs b/src/PostIt/PostIt/ViewModels/ActivitiesPageViewModel.cs
similarity index 100%
rename from src/PostIt/PostIt/ViewModels/Activity/ActivitiesPageViewModel.cs
rename to src/PostIt/PostIt/ViewModels/ActivitiesPageViewModel.cs
diff --git a/src/PostIt/PostIt/ViewModels/Activity/EstimateEditionPageViewModel.cs b/src/PostIt/PostIt/ViewModels/Activity/EstimateEditionPageViewModel.cs
deleted file mode 100644
index 8f007dc08..000000000
--- a/src/PostIt/PostIt/ViewModels/Activity/EstimateEditionPageViewModel.cs
+++ /dev/null
@@ -1,293 +0,0 @@
-using System;
-using System.Collections.ObjectModel;
-using System.Collections.Specialized;
-using System.ComponentModel;
-using System.Linq;
-using System.Net;
-using System.Net.Http;
-using System.Threading.Tasks;
-using Avalonia;
-using CommunityToolkit.Mvvm.ComponentModel;
-using CommunityToolkit.Mvvm.Input;
-using PostIt.Helpers;
-using Yavsc.Api.Client;
-
-namespace PostIt.ViewModels;
-
-///
-/// Edition d'un devis (Estimate) créé en réponse à une demande
-/// client () consultée depuis la
-/// page « Mes demandes en cours ». L'envoi poste le devis sur
-/// api/v1/estimate; côté serveur, la commande liée
-/// () est alors marquée comme
-/// validée par le prestataire.
-///
-public partial class EstimateEditionPageViewModel : ViewModelBase, IActionStatusViewModel
-{
- private readonly EstimateApiClient _estimateClient;
- private readonly BillingQuerySummaryDto _query;
-
- public long QueryId => _query.Id;
- public string ClientId => _query.ClientId;
- public string BillingCode => _query.BillingCode;
-
- public string Title => $"Devis — demande #{_query.Id}";
-
- public string ContextLabel
- => $"Demande #{_query.Id} · {BillingCode} · client {ClientId}";
-
- public string QueryDescription => string.IsNullOrWhiteSpace(_query.Description)
- ? "(sans description)"
- : _query.Description;
-
- [ObservableProperty]
- public partial string EstimateTitle { get; set; } = string.Empty;
-
- [ObservableProperty]
- public partial string EstimateDescription { get; set; } = string.Empty;
-
- [ObservableProperty]
- public partial ObservableCollection Lines { get; set; } = new();
-
- [ObservableProperty, NotifyCanExecuteChangedFor(nameof(RemoveLineCommand))]
- public partial EstimateLineItemViewModel? SelectedLine { get; set; }
-
- [ObservableProperty, NotifyCanExecuteChangedFor(nameof(SendCommand))]
- public partial bool IsBusy { get; set; }
-
- ///
- /// True une fois le devis accepté par le serveur: l'envoi est
- /// désactivé pour éviter les doublons, il ne reste que « Retour ».
- ///
- [ObservableProperty, NotifyCanExecuteChangedFor(nameof(SendCommand))]
- public partial bool HasSent { get; set; }
-
- [ObservableProperty]
- public partial string StatusMessage { get; set; } = "Prêt.";
-
- [ObservableProperty]
- public partial StatusNotice ActionStatus { get; set; } = StatusNotice.Info("Prêt.");
-
- public decimal Total => Lines.Sum(line => line.LineTotal);
-
- public string TotalLabel => $"{Total:0.00} {Lines.FirstOrDefault()?.Currency ?? "EUR"}";
-
- public string SendLabel => HasSent ? "Devis envoyé" : "Envoyer le devis";
-
- public override bool CanNavigateNext
- {
- get => false;
- protected set { _ = value; }
- }
-
- public override bool CanNavigatePrevious
- {
- get => true;
- protected set { _ = value; }
- }
-
- public EstimateEditionPageViewModel(BillingQuerySummaryDto query, EstimateApiClient estimateClient)
- {
- _query = query ?? throw new ArgumentNullException(nameof(query));
- _estimateClient = estimateClient ?? throw new ArgumentNullException(nameof(estimateClient));
-
- EstimateDescription = query.Description ?? string.Empty;
- Lines.CollectionChanged += OnLinesCollectionChanged;
-
- AddLine();
- this.SetInfoStatus("Complétez le devis puis envoyez-le. La demande associée sera validée.");
- }
-
- [RelayCommand]
- private void AddLine()
- {
- var line = new EstimateLineItemViewModel();
- Lines.Add(line);
- SelectedLine = line;
- }
-
- private bool CanRemoveLine() => SelectedLine is not null && !IsBusy && !HasSent;
-
- [RelayCommand(CanExecute = nameof(CanRemoveLine))]
- private void RemoveLine()
- {
- if (SelectedLine is null)
- {
- return;
- }
-
- var index = Lines.IndexOf(SelectedLine);
- Lines.Remove(SelectedLine);
- SelectedLine = Lines.Count == 0
- ? null
- : Lines[Math.Min(index, Lines.Count - 1)];
- }
-
- private bool CanSend() => !IsBusy && !HasSent;
-
- [RelayCommand(CanExecute = nameof(CanSend))]
- private async Task SendAsync()
- {
- if (!TryValidate(out var validationMessage))
- {
- this.SetWarningStatus(validationMessage);
- return;
- }
-
- IsBusy = true;
- try
- {
- var payload = BuildPayload();
- var created = await _estimateClient.CreateAsync(payload).ConfigureAwait(true);
-
- HasSent = true;
- OnPropertyChanged(nameof(SendLabel));
- this.SetInfoStatus(
- $"Devis #{created.Id} envoyé ({created.Bill.Count} ligne(s)). La demande #{QueryId} est validée.");
- }
- catch (HttpRequestException ex) when (ex.StatusCode is HttpStatusCode.Unauthorized or HttpStatusCode.Forbidden)
- {
- this.SetWarningStatus("Accès refusé à l'API devis (scope 'api'). Déconnectez puis reconnectez-vous.");
- }
- catch (Exception ex)
- {
- this.SetErrorStatus($"Erreur lors de l'envoi du devis: {ex.Message}");
- }
- finally
- {
- IsBusy = false;
- }
- }
-
- [RelayCommand]
- private async Task BackAsync()
- {
- var app = (App?)Application.Current;
- if (app is null)
- {
- throw new InvalidOperationException("Application PostIt indisponible.");
- }
-
- await app.GoBackAsync().ConfigureAwait(true);
- }
-
- internal EstimateDto BuildPayload()
- {
- return new EstimateDto
- {
- CommandId = QueryId,
- ClientId = ClientId,
- CommandType = BillingCode,
- Title = EstimateTitle.Trim(),
- Description = EstimateDescription.Trim(),
- Bill = Lines.Select(line => new EstimateLineDto
- {
- Id = line.Id,
- Name = line.Name.Trim(),
- Description = line.Description.Trim(),
- Count = Math.Max(1, (int)Math.Round(line.Count)),
- UnitaryCost = line.UnitaryCost,
- Currency = string.IsNullOrWhiteSpace(line.Currency) ? "EUR" : line.Currency.Trim(),
- }).ToList(),
- };
- }
-
- private bool TryValidate(out string message)
- {
- if (string.IsNullOrWhiteSpace(EstimateTitle))
- {
- message = "Le titre du devis est requis.";
- return false;
- }
-
- if (string.IsNullOrWhiteSpace(ClientId))
- {
- message = "La demande sélectionnée n'identifie pas de client.";
- return false;
- }
-
- if (string.IsNullOrWhiteSpace(BillingCode))
- {
- message = "La demande sélectionnée n'a pas de code de facturation.";
- return false;
- }
-
- if (Lines.Count == 0)
- {
- message = "Ajoutez au moins une ligne au devis.";
- return false;
- }
-
- foreach (var line in Lines)
- {
- if (string.IsNullOrWhiteSpace(line.Name))
- {
- message = "Chaque ligne doit avoir un nom.";
- return false;
- }
-
- if (line.Name.Trim().Length > 256)
- {
- message = $"Le nom de la ligne « {line.Name.Trim()[..20]}… » dépasse 256 caractères.";
- return false;
- }
-
- if (string.IsNullOrWhiteSpace(line.Description))
- {
- message = $"La ligne « {line.Name.Trim()} » doit avoir une description.";
- return false;
- }
-
- if (line.Description.Trim().Length > 512)
- {
- message = $"La description de la ligne « {line.Name.Trim()} » dépasse 512 caractères.";
- return false;
- }
-
- if (line.Count < 1)
- {
- message = $"La quantité de la ligne « {line.Name.Trim()} » doit être d'au moins 1.";
- return false;
- }
- }
-
- message = string.Empty;
- return true;
- }
-
- private void OnLinesCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
- {
- if (e.OldItems is not null)
- {
- foreach (var item in e.OldItems.OfType())
- {
- item.PropertyChanged -= OnLinePropertyChanged;
- }
- }
-
- if (e.NewItems is not null)
- {
- foreach (var item in e.NewItems.OfType())
- {
- item.PropertyChanged += OnLinePropertyChanged;
- }
- }
-
- RaiseTotalsChanged();
- }
-
- private void OnLinePropertyChanged(object? sender, PropertyChangedEventArgs e)
- {
- if (e.PropertyName is nameof(EstimateLineItemViewModel.LineTotal)
- or nameof(EstimateLineItemViewModel.Currency))
- {
- RaiseTotalsChanged();
- }
- }
-
- private void RaiseTotalsChanged()
- {
- OnPropertyChanged(nameof(Total));
- OnPropertyChanged(nameof(TotalLabel));
- }
-}
diff --git a/src/PostIt/PostIt/ViewModels/Activity/EstimateLineItemViewModel.cs b/src/PostIt/PostIt/ViewModels/Activity/EstimateLineItemViewModel.cs
deleted file mode 100644
index 864906cdd..000000000
--- a/src/PostIt/PostIt/ViewModels/Activity/EstimateLineItemViewModel.cs
+++ /dev/null
@@ -1,35 +0,0 @@
-using CommunityToolkit.Mvvm.ComponentModel;
-
-namespace PostIt.ViewModels;
-
-///
-/// Editable estimate line. is exposed as a
-/// so it binds directly to
-/// NumericUpDown.Value (decimal?); it is rounded back
-/// to an integer when the DTO is built.
-///
-public partial class EstimateLineItemViewModel : ObservableObject
-{
- public long Id { get; set; }
-
- [ObservableProperty]
- public partial string Name { get; set; } = string.Empty;
-
- [ObservableProperty]
- public partial string Description { get; set; } = string.Empty;
-
- [ObservableProperty, NotifyPropertyChangedFor(nameof(LineTotal))]
- [NotifyPropertyChangedFor(nameof(LineTotalLabel))]
- public partial decimal Count { get; set; } = 1m;
-
- [ObservableProperty, NotifyPropertyChangedFor(nameof(LineTotal))]
- [NotifyPropertyChangedFor(nameof(LineTotalLabel))]
- public partial decimal UnitaryCost { get; set; }
-
- [ObservableProperty]
- public partial string Currency { get; set; } = "EUR";
-
- public decimal LineTotal => Count * UnitaryCost;
-
- public string LineTotalLabel => $"{LineTotal:0.00}";
-}
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/Activity/ProviderOngoingRequestsPageViewModel.cs b/src/PostIt/PostIt/ViewModels/Activity/ProviderOngoingRequestsPageViewModel.cs
deleted file mode 100644
index 71e979e66..000000000
--- a/src/PostIt/PostIt/ViewModels/Activity/ProviderOngoingRequestsPageViewModel.cs
+++ /dev/null
@@ -1,390 +0,0 @@
-using System;
-using System.Collections.ObjectModel;
-using System.Collections.Generic;
-using System.Linq;
-using System.Net;
-using System.Net.Http;
-using System.Threading.Tasks;
-using Avalonia;
-using CommunityToolkit.Mvvm.ComponentModel;
-using CommunityToolkit.Mvvm.Input;
-using PostIt.Helpers;
-using Yavsc;
-using Yavsc.Abstract.Workflow;
-using Yavsc.Api.Client;
-
-namespace PostIt.ViewModels;
-
-public partial class ProviderOngoingRequestsPageViewModel : ViewModelBase, IActionStatusViewModel
-{
- public const string SortByDate = "Date (plus récent d'abord)";
- public const string SortByDateAsc = "Date (plus ancien d'abord)";
- public const string SortByStatus = "Statut (en cours d'abord)";
-
- private readonly BillingApiClient _billingClient;
- private readonly EstimateApiClient? _estimateClient;
- private readonly Settings? _settings;
- private List _allQueries = new();
-
- [ObservableProperty]
- public partial ObservableCollection Queries { get; set; } = new();
-
- [ObservableProperty]
- public partial string FilterText { get; set; } = string.Empty;
-
- public IReadOnlyList SortOptions { get; } = new[]
- {
- SortByDate,
- SortByDateAsc,
- SortByStatus,
- };
-
- [ObservableProperty]
- public partial string SelectedSortOption { get; set; } = SortByDate;
-
- [ObservableProperty, NotifyCanExecuteChangedFor(nameof(OpenSelectedQueryCommand))]
- [NotifyCanExecuteChangedFor(nameof(OpenSelectedEditorCommand))]
- [NotifyCanExecuteChangedFor(nameof(CreateEstimateForSelectedCommand))]
- public partial BillingQuerySummaryDto? SelectedQuery { get; set; }
-
- [ObservableProperty]
- public partial bool IsBusy { get; set; }
-
- [ObservableProperty]
- public partial string StatusMessage { get; set; } = "Chargement des demandes fournisseur...";
-
- [ObservableProperty]
- public partial StatusNotice ActionStatus { get; set; } = StatusNotice.Info("Chargement des demandes fournisseur...");
-
- public string Title => "Mes demandes en cours";
-
- public override bool CanNavigateNext
- {
- get => false;
- protected set { _ = value; }
- }
-
- public override bool CanNavigatePrevious
- {
- get => true;
- protected set { _ = value; }
- }
-
- public ProviderOngoingRequestsPageViewModel(
- BillingApiClient billingClient,
- Settings? settings = null,
- EstimateApiClient? estimateClient = null)
- {
- _billingClient = billingClient ?? throw new ArgumentNullException(nameof(billingClient));
- _estimateClient = estimateClient;
- _settings = settings;
-
- if (_settings is not null)
- {
- var preferredSort = NormalizeSortOption(_settings.ProviderOngoingRequestsSortOption);
- if (!string.Equals(preferredSort, SelectedSortOption, StringComparison.Ordinal))
- {
- SelectedSortOption = preferredSort;
- }
- }
- }
-
- public Task InitializeAsync() => RefreshAsync();
-
- [RelayCommand]
- public async Task RefreshAsync()
- {
- IsBusy = true;
- try
- {
- var items = await _billingClient.GetProviderOngoingQueriesAsync().ConfigureAwait(true) ?? new();
- _allQueries = items
- .Where(x => !string.IsNullOrWhiteSpace(x.BillingCode))
- .OrderByDescending(x => x.EventDate ?? DateTime.MinValue)
- .ThenByDescending(x => x.Id)
- .ToList();
-
- ApplyFilter();
- this.SetInfoStatus(_allQueries.Count == 0
- ? "Aucune demande en cours pour votre profil fournisseur."
- : $"{_allQueries.Count} demande(s) en cours chargée(s).");
- }
- catch (HttpRequestException ex) when (ex.StatusCode is HttpStatusCode.Unauthorized or HttpStatusCode.Forbidden)
- {
- _allQueries = new List();
- Queries = new ObservableCollection();
- this.SetWarningStatus("Accès refusé au billing (scope 'api'). Déconnectez puis reconnectez-vous.");
- }
- catch (Exception ex)
- {
- _allQueries = new List();
- Queries = new ObservableCollection();
- this.SetErrorStatus($"Erreur: {ex.Message}");
- }
- finally
- {
- IsBusy = false;
- }
- }
-
- private bool CanOpenSelectedQuery() => SelectedQuery is not null;
-
- private bool CanOpenSelectedEditor() => SelectedQuery is not null;
-
- [RelayCommand(CanExecute = nameof(CanOpenSelectedQuery))]
- public async Task OpenSelectedQueryAsync()
- {
- if (SelectedQuery is null)
- {
- this.SetWarningStatus("Sélectionnez une demande.");
- return;
- }
-
- var app = (App?)Application.Current;
- if (app is null)
- {
- throw new InvalidOperationException("Application PostIt indisponible.");
- }
-
- IsBusy = true;
- try
- {
- var details = await _billingClient
- .GetQueryAsync(SelectedQuery.BillingCode, SelectedQuery.Id)
- .ConfigureAwait(true);
- var (activity, performer, form) = BuildNavigationContext(SelectedQuery);
-
- var vm = new BillingQueryDetailsPageViewModel(
- activity,
- performer,
- form,
- _billingClient,
- details,
- isReadOnly: false);
-
- await app.PushPageAsync(vm).ConfigureAwait(true);
- }
- catch (Exception ex)
- {
- this.SetErrorStatus($"Erreur lors de l'ouverture: {ex.Message}");
- }
- finally
- {
- IsBusy = false;
- }
- }
-
- [RelayCommand(CanExecute = nameof(CanOpenSelectedEditor))]
- public async Task OpenSelectedEditorAsync()
- {
- if (SelectedQuery is null)
- {
- this.SetWarningStatus("Sélectionnez une demande.");
- return;
- }
-
- var app = (App?)Application.Current;
- if (app is null)
- {
- throw new InvalidOperationException("Application PostIt indisponible.");
- }
-
- IsBusy = true;
- try
- {
- var details = await _billingClient
- .GetQueryAsync(SelectedQuery.BillingCode, SelectedQuery.Id)
- .ConfigureAwait(true);
-
- var (activity, performer, form) = BuildNavigationContext(SelectedQuery);
- var vm = form.CreateCommandPageViewModel(activity, performer, _billingClient);
- if (vm is null)
- {
- this.SetWarningStatus($"Le formulaire '{form.ActionName}' n'est pas pris en charge en édition.");
- return;
- }
-
- await vm.InitializeAsync(details).ConfigureAwait(true);
- await app.PushPageAsync(vm).ConfigureAwait(true);
- }
- catch (Exception ex)
- {
- this.SetErrorStatus($"Erreur lors de l'ouverture en édition: {ex.Message}");
- }
- finally
- {
- IsBusy = false;
- }
- }
-
- private bool CanCreateEstimateForSelected() => SelectedQuery is not null && _estimateClient is not null;
-
- [RelayCommand(CanExecute = nameof(CanCreateEstimateForSelected))]
- public async Task CreateEstimateForSelectedAsync()
- {
- if (SelectedQuery is null)
- {
- this.SetWarningStatus("Sélectionnez une demande.");
- return;
- }
-
- if (_estimateClient is null)
- {
- this.SetWarningStatus("Le client devis n'est pas disponible.");
- return;
- }
-
- var app = (App?)Application.Current;
- if (app is null)
- {
- throw new InvalidOperationException("Application PostIt indisponible.");
- }
-
- var vm = new EstimateEditionPageViewModel(SelectedQuery, _estimateClient);
- await app.PushPageAsync(vm).ConfigureAwait(true);
- }
-
- partial void OnFilterTextChanged(string value)
- {
- ApplyFilter();
- }
-
- partial void OnSelectedSortOptionChanged(string value)
- {
- var normalized = NormalizeSortOption(value);
- if (!string.Equals(normalized, value, StringComparison.Ordinal))
- {
- SelectedSortOption = normalized;
- return;
- }
-
- PersistSortPreference(value);
- ApplyFilter();
- }
-
- private void ApplyFilter()
- {
- var query = FilterText?.Trim();
- var filtered = string.IsNullOrWhiteSpace(query)
- ? _allQueries
- : _allQueries.Where(x =>
- ContainsInsensitive(x.Description, query)
- || ContainsInsensitive(x.ActivityCode, query)
- || ContainsInsensitive(x.BillingCode, query)
- || ContainsInsensitive(x.ClientId, query)
- || ContainsInsensitive(x.Status.ToString(), query))
- .ToList();
-
- var sorted = ApplySort(filtered);
- Queries = new ObservableCollection(sorted);
- }
-
- private List ApplySort(IEnumerable source)
- {
- if (string.Equals(SelectedSortOption, SortByStatus, StringComparison.Ordinal))
- {
- return source
- .OrderBy(x => GetStatusRank(x.Status))
- .ThenByDescending(x => x.EventDate ?? DateTime.MinValue)
- .ThenByDescending(x => x.Id)
- .ToList();
- }
-
- if (string.Equals(SelectedSortOption, SortByDateAsc, StringComparison.Ordinal))
- {
- return source
- .OrderBy(x => x.EventDate ?? DateTime.MinValue)
- .ThenBy(x => x.Id)
- .ToList();
- }
-
- return source
- .OrderByDescending(x => x.EventDate ?? DateTime.MinValue)
- .ThenByDescending(x => x.Id)
- .ToList();
- }
-
- private void PersistSortPreference(string selectedSort)
- {
- if (_settings is null)
- {
- return;
- }
-
- if (string.Equals(_settings.ProviderOngoingRequestsSortOption, selectedSort, StringComparison.Ordinal))
- {
- return;
- }
-
- _settings.ProviderOngoingRequestsSortOption = selectedSort;
-
- try
- {
- _settings.Save();
- }
- catch
- {
- this.SetWarningStatus("Le tri a été appliqué, mais sa sauvegarde a échoué.");
- }
- }
-
- private static string NormalizeSortOption(string? sortOption)
- {
- if (string.Equals(sortOption, SortByDate, StringComparison.Ordinal)
- || string.Equals(sortOption, SortByDateAsc, StringComparison.Ordinal)
- || string.Equals(sortOption, SortByStatus, StringComparison.Ordinal))
- {
- return sortOption!;
- }
-
- return SortByDate;
- }
-
- private static int GetStatusRank(QueryStatus status)
- => status switch
- {
- QueryStatus.InProgress => 0,
- QueryStatus.Accepted => 1,
- QueryStatus.Inserted => 2,
- QueryStatus.Success => 3,
- QueryStatus.Rejected => 4,
- QueryStatus.Failed => 5,
- _ => 99,
- };
-
- private static bool ContainsInsensitive(string? source, string query)
- => !string.IsNullOrWhiteSpace(source)
- && source.Contains(query, StringComparison.OrdinalIgnoreCase);
-
- private static (ActivityInfo activity, ActivityUserDisplayItem performer, CommandFormSummary form)
- BuildNavigationContext(BillingQuerySummaryDto query)
- {
- var activity = new ActivityInfo
- {
- Code = query.ActivityCode,
- Name = string.IsNullOrWhiteSpace(query.ActivityCode)
- ? "Activité"
- : query.ActivityCode,
- };
-
- var performer = new ActivityUserDisplayItem
- {
- PerformerId = query.PerformerId,
- UserName = "Mon profil fournisseur",
- AvatarFallbackLabel = "M",
- IsPerformerActive = true,
- PerformerStatusBadgeLabel = "Actif",
- PerformerStatusBadgeBackground = "#E6F7EC",
- PerformerStatusBadgeBorder = "#2E7D32",
- PerformerStatusBadgeForeground = "#1B5E20",
- };
-
- var form = new CommandFormSummary
- {
- ActionName = query.BillingCode,
- Title = query.BillingCode,
- };
-
- return (activity, performer, form);
- }
-}
diff --git a/src/PostIt/PostIt/ViewModels/Activity/ActivityUserDisplayItem.cs b/src/PostIt/PostIt/ViewModels/ActivityUserDisplayItem.cs
similarity index 100%
rename from src/PostIt/PostIt/ViewModels/Activity/ActivityUserDisplayItem.cs
rename to src/PostIt/PostIt/ViewModels/ActivityUserDisplayItem.cs
diff --git a/src/PostIt/PostIt/ViewModels/ACL/AddCircleMemberDialogViewModel.cs b/src/PostIt/PostIt/ViewModels/AddCircleMemberDialogViewModel.cs
similarity index 100%
rename from src/PostIt/PostIt/ViewModels/ACL/AddCircleMemberDialogViewModel.cs
rename to src/PostIt/PostIt/ViewModels/AddCircleMemberDialogViewModel.cs
diff --git a/src/PostIt/PostIt/ViewModels/Activity/BillingQueriesPageViewModel.cs b/src/PostIt/PostIt/ViewModels/BillingQueriesPageViewModel.cs
similarity index 97%
rename from src/PostIt/PostIt/ViewModels/Activity/BillingQueriesPageViewModel.cs
rename to src/PostIt/PostIt/ViewModels/BillingQueriesPageViewModel.cs
index aab923d80..bccf97167 100644
--- a/src/PostIt/PostIt/ViewModels/Activity/BillingQueriesPageViewModel.cs
+++ b/src/PostIt/PostIt/ViewModels/BillingQueriesPageViewModel.cs
@@ -43,7 +43,7 @@ public partial class BillingQueriesPageViewModel : ViewModelBase, IActionStatusV
? $"Demandes en cours ({Form.Title})"
: $"Commandes {Form.Title}";
public string ContextLabel => $"{Performer.UserName} · {Activity.Name}";
- public bool CanOpenDetails => !IsReadOnly;
+ public bool CanOpenDetails => true;
public override bool CanNavigateNext
{
@@ -75,7 +75,7 @@ public partial class BillingQueriesPageViewModel : ViewModelBase, IActionStatusV
public Task InitializeAsync() => RefreshAsync();
- private bool CanOpenSelectedQuery() => CanOpenDetails && SelectedQuery is not null;
+ private bool CanOpenSelectedQuery() => SelectedQuery is not null;
[RelayCommand]
public async Task RefreshAsync()
diff --git a/src/PostIt/PostIt/ViewModels/Activity/BillingQueryDetailsPageViewModel.cs b/src/PostIt/PostIt/ViewModels/BillingQueryDetailsPageViewModel.cs
similarity index 100%
rename from src/PostIt/PostIt/ViewModels/Activity/BillingQueryDetailsPageViewModel.cs
rename to src/PostIt/PostIt/ViewModels/BillingQueryDetailsPageViewModel.cs
diff --git a/src/PostIt/PostIt/ViewModels/Activity/BillingQueryDisplayItem.cs b/src/PostIt/PostIt/ViewModels/BillingQueryDisplayItem.cs
similarity index 100%
rename from src/PostIt/PostIt/ViewModels/Activity/BillingQueryDisplayItem.cs
rename to src/PostIt/PostIt/ViewModels/BillingQueryDisplayItem.cs
diff --git a/src/PostIt/PostIt/ViewModels/ACL/CirclesPageViewModel.cs b/src/PostIt/PostIt/ViewModels/CirclesPageViewModel.cs
similarity index 100%
rename from src/PostIt/PostIt/ViewModels/ACL/CirclesPageViewModel.cs
rename to src/PostIt/PostIt/ViewModels/CirclesPageViewModel.cs
diff --git a/src/PostIt/PostIt/ViewModels/Activity/CommandFormsPageViewModel.cs b/src/PostIt/PostIt/ViewModels/CommandFormsPageViewModel.cs
similarity index 100%
rename from src/PostIt/PostIt/ViewModels/Activity/CommandFormsPageViewModel.cs
rename to src/PostIt/PostIt/ViewModels/CommandFormsPageViewModel.cs
diff --git a/src/PostIt/PostIt/ViewModels/Commands/RdvViewModel.cs b/src/PostIt/PostIt/ViewModels/Commands/RdvViewModel.cs
index 4f9d457a5..7a7d17df6 100644
--- a/src/PostIt/PostIt/ViewModels/Commands/RdvViewModel.cs
+++ b/src/PostIt/PostIt/ViewModels/Commands/RdvViewModel.cs
@@ -12,9 +12,6 @@ namespace PostIt.ViewModels.Commands;
public partial class RdvViewModel : BillingCommandPageViewModel
{
- private long? _existingLocationId;
- private bool _hydratingExistingQuery;
-
public override string SupportMessage => "Complétez les informations du rendez-vous puis postez la commande.";
[ObservableProperty]
@@ -59,7 +56,6 @@ public partial class RdvViewModel : BillingCommandPageViewModel
protected override void ApplyExistingQuery(BillingQueryDetailsDto existingQuery)
{
- _hydratingExistingQuery = true;
ExistingQueryId = existingQuery.Id;
CommandStatus = existingQuery.Status;
Consent = existingQuery.Consent;
@@ -74,18 +70,11 @@ public partial class RdvViewModel : BillingCommandPageViewModel
if (existingQuery.Location is not null)
{
- _existingLocationId = existingQuery.Location.Id;
Address = existingQuery.Location.Address ?? string.Empty;
SuggestedAddress = string.Empty;
Latitude = existingQuery.Location.Latitude;
Longitude = existingQuery.Location.Longitude;
}
- else
- {
- _existingLocationId = null;
- }
-
- _hydratingExistingQuery = false;
this.SetInfoStatus($"Commande #{existingQuery.Id} chargée.");
}
@@ -128,22 +117,20 @@ public partial class RdvViewModel : BillingCommandPageViewModel
}
}
- protected static BillingLocationDto BuildLocationPayload(string address, double? latitude, double? longitude, long? locationId = null)
+ protected static object BuildLocationPayload(string address, double? latitude, double? longitude)
{
if (latitude.HasValue && longitude.HasValue)
{
- return new BillingLocationDto
+ return new
{
- Id = locationId,
Address = address,
Latitude = latitude.Value,
Longitude = longitude.Value,
};
}
- return new BillingLocationDto
+ return new
{
- Id = locationId,
Address = address,
};
}
@@ -236,30 +223,6 @@ public partial class RdvViewModel : BillingCommandPageViewModel
OnPropertyChanged(nameof(EventDateSelection));
}
- partial void OnAddressChanged(string value)
- {
- if (_hydratingExistingQuery)
- return;
-
- _existingLocationId = null;
- }
-
- partial void OnLatitudeChanged(double? value)
- {
- if (_hydratingExistingQuery)
- return;
-
- _existingLocationId = null;
- }
-
- partial void OnLongitudeChanged(double? value)
- {
- if (_hydratingExistingQuery)
- return;
-
- _existingLocationId = null;
- }
-
protected override async Task SubmitAsync()
{
@@ -294,7 +257,7 @@ public partial class RdvViewModel : BillingCommandPageViewModel
try
{
var address = Address.Trim();
- var locationPayload = BuildLocationPayload(address, Latitude, Longitude, IsEditingExisting ? _existingLocationId : null);
+ var locationPayload = BuildLocationPayload(address, Latitude, Longitude);
var payload = new BillingQueryDetailsDto
{
@@ -307,7 +270,12 @@ public partial class RdvViewModel : BillingCommandPageViewModel
Status = CommandStatus,
Reason = Reason.Trim(),
AdditionalInfo = string.IsNullOrWhiteSpace(AdditionalInfo) ? string.Empty : AdditionalInfo.Trim(),
- Location = locationPayload
+ Location = new BillingLocationDto
+ {
+ Address = address,
+ Latitude = Latitude,
+ Longitude = Longitude,
+ }
};
if (IsEditingExisting)
diff --git a/src/PostIt/PostIt/ViewModels/Layout/HomePageViewModel.cs b/src/PostIt/PostIt/ViewModels/HomePageViewModel.cs
similarity index 51%
rename from src/PostIt/PostIt/ViewModels/Layout/HomePageViewModel.cs
rename to src/PostIt/PostIt/ViewModels/HomePageViewModel.cs
index ea92f1d0a..830be4dde 100644
--- a/src/PostIt/PostIt/ViewModels/Layout/HomePageViewModel.cs
+++ b/src/PostIt/PostIt/ViewModels/HomePageViewModel.cs
@@ -5,7 +5,6 @@ using CommunityToolkit.Mvvm.Input;
using Microsoft.Extensions.DependencyInjection;
using PostIt.Helpers;
using PostIt.Services;
-using Yavsc.Api.Client;
namespace PostIt.ViewModels;
public class HomePageViewModel : ViewModelBase
@@ -31,16 +30,10 @@ public class HomePageViewModel : ViewModelBase
SessionStatus = sessionStatus;
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 OpenBlogs { get; } = new AsyncRelayCommand(App.PushBlogsPageAsync);
public IAsyncRelayCommand OpenActivities { get; }
- public IAsyncRelayCommand OpenProviderRequests { get; }
- public IAsyncRelayCommand OpenClientEstimates { get; }
- public IAsyncRelayCommand OpenProviderEstimates { get; }
private async Task OpenActivitiesAsync()
{
@@ -55,46 +48,6 @@ public class HomePageViewModel : ViewModelBase
await app.PushPageAsync(vm);
}
- private async Task OpenProviderRequestsAsync()
- {
- var app = (App?)Application.Current;
- if (app is null)
- {
- throw new InvalidOperationException("Application PostIt indisponible.");
- }
-
- var billingClient = app.ServiceProvider?.GetRequiredService();
- if (billingClient is null)
- {
- throw new InvalidOperationException("Client billing indisponible.");
- }
-
- var estimateClient = app.ServiceProvider?.GetRequiredService();
-
- var vm = new ProviderOngoingRequestsPageViewModel(billingClient, Settings, estimateClient);
- await vm.InitializeAsync();
- 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/Blogs/BlogsViewModel.cs b/src/PostIt/PostIt/ViewModels/MainViewModel.cs
similarity index 84%
rename from src/PostIt/PostIt/ViewModels/Blogs/BlogsViewModel.cs
rename to src/PostIt/PostIt/ViewModels/MainViewModel.cs
index 726129f12..d2667c012 100644
--- a/src/PostIt/PostIt/ViewModels/Blogs/BlogsViewModel.cs
+++ b/src/PostIt/PostIt/ViewModels/MainViewModel.cs
@@ -1,6 +1,5 @@
using System;
using System.Collections.ObjectModel;
-using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Avalonia;
@@ -9,12 +8,11 @@ using CommunityToolkit.Mvvm.Input;
using Microsoft.Extensions.DependencyInjection;
using Yavsc.Blogspot;
using Yavsc.Api.Client;
-using Yavsc.Abstract.Files;
using PostIt.Helpers;
namespace PostIt.ViewModels;
-public partial class BlogsViewModel : ViewModelBase, IActionStatusViewModel
+public partial class MainViewModel : ViewModelBase, IActionStatusViewModel
{
/// Window/tab title. Cosmetic — bound by
/// MainPage.axaml if at all. Not the post title.
@@ -68,9 +66,6 @@ public partial class BlogsViewModel : ViewModelBase, IActionStatusViewModel
[ObservableProperty]
public partial ObservableCollection FilteredPosts { get; set; }
- [ObservableProperty]
- public partial ObservableCollection DraftAttachments { get; set; }
-
[ObservableProperty]
public partial BlogPostDto? SelectedPost { get; set; }
@@ -80,9 +75,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()
{
@@ -99,6 +91,12 @@ public partial class BlogsViewModel : ViewModelBase, IActionStatusViewModel
});
}
+ [RelayCommand]
+ internal async Task SearchAsync() {
+ await RefreshAsync();
+ ApplyFilter();
+ }
+
[RelayCommand]
internal async Task SaveAsync()
{
@@ -115,11 +113,9 @@ public partial class BlogsViewModel : ViewModelBase, IActionStatusViewModel
await ExecuteAsync(async () =>
{
- var attachments = DraftAttachments.ToArray();
-
// 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
@@ -137,33 +133,15 @@ public partial class BlogsViewModel : ViewModelBase, IActionStatusViewModel
DateModified = DateTime.UtcNow,
IsPublished = DraftIsPublished
};
- var created = await BlogClient!.CreatePostAsync(draft, attachments);
+ var created = await BlogClient!.CreatePostAsync(draft);
if (created is not null)
{
SelectedPost = created;
-
- if (TryAppendAttachmentLinks(created, attachments))
- {
- var linkUpdate = new BlogPostDto
- {
- Id = created.Id,
- AuthorId = created.AuthorId,
- Photo = created.Photo,
- Title = DraftTitle,
- Article = DraftArticle ?? string.Empty,
- DateCreated = created.DateCreated,
- DateModified = DateTime.UtcNow,
- };
- await BlogClient.UpdatePostAsync(created.Id, linkUpdate);
- }
-
this.SetInfoStatus($"Billet {created.Id} créé.");
- DraftAttachments.Clear();
}
}
else
{
- TryAppendAttachmentLinks(SelectedPost, attachments);
var update = new BlogPostDto
{
Id = SelectedPost.Id,
@@ -174,9 +152,8 @@ public partial class BlogsViewModel : ViewModelBase, IActionStatusViewModel
DateCreated = SelectedPost.DateCreated,
DateModified = DateTime.UtcNow,
};
- await BlogClient!.UpdatePostAsync(SelectedPost.Id, update, attachments);
+ await BlogClient!.UpdatePostAsync(SelectedPost.Id, update);
this.SetInfoStatus($"Billet {SelectedPost.Id} enregistré.");
- DraftAttachments.Clear();
}
await RefreshPostsAsync();
@@ -359,9 +336,8 @@ public partial class BlogsViewModel : ViewModelBase, IActionStatusViewModel
}
- public BlogsViewModel()
+ public MainViewModel()
{
- WindowTitle = "Blog";
SettingsModel = new Settings();
Init(SettingsModel);
BlogClient = null;
@@ -371,13 +347,11 @@ public partial class BlogsViewModel : ViewModelBase, IActionStatusViewModel
{
Posts = new ObservableCollection();
FilteredPosts = new ObservableCollection();
- DraftAttachments = new ObservableCollection();
SelectedPost = null;
IsBusy = false;
this.SetInfoStatus("Prêt.");
Settings = settings ?? new Settings();
SearchText = Settings.SearchText;
- IsMarkdownViewerVisible = false;
WindowTitle = "PostIt";
DraftTitle = string.Empty;
DraftArticle = string.Empty;
@@ -422,9 +396,8 @@ public partial class BlogsViewModel : ViewModelBase, IActionStatusViewModel
/// . Production code uses the
/// (Settings, BlogApiClient) overload below.
///
- public BlogsViewModel(BlogApiClient blogClient, Settings? settings = null, IServiceProvider? services = null)
+ public MainViewModel(BlogApiClient blogClient, Settings? settings = null, IServiceProvider? services = null)
{
- WindowTitle = "Blog";
SettingsModel = new Settings();
BlogClient = blogClient ?? throw new ArgumentNullException(nameof(blogClient)); ;
Services = services;
@@ -454,7 +427,6 @@ public partial class BlogsViewModel : ViewModelBase, IActionStatusViewModel
// Mirror publication state too. Defaults to false on
// null selection so a fresh draft starts unpublished.
DraftIsPublished = value?.IsPublished ?? false;
- DraftAttachments.Clear();
UpdateCommandStates();
}
@@ -536,55 +508,4 @@ public partial class BlogsViewModel : ViewModelBase, IActionStatusViewModel
IsLoaded = true;
}
}
-
- private bool TryAppendAttachmentLinks(BlogPostDto post, IReadOnlyCollection attachments)
- {
- if (attachments.Count == 0)
- return false;
-
- var ownerSegment = post.Author?.UserName;
- if (string.IsNullOrWhiteSpace(ownerSegment))
- ownerSegment = post.AuthorId;
-
- if (string.IsNullOrWhiteSpace(ownerSegment))
- return false;
-
- var article = DraftArticle ?? string.Empty;
- var links = new List();
-
- foreach (var attachment in attachments)
- {
- var relativePath = $"{EscapePathSegment(ownerSegment)}/blogs/{post.Id}/{EscapePathSegment(attachment.FileName)}";
- var fileUrl = ResolveUserFileUrl(relativePath);
- var markdownLine = $"- [{attachment.FileName}]({fileUrl})";
-
- if (!article.Contains(markdownLine, StringComparison.Ordinal))
- links.Add(markdownLine);
- }
-
- if (links.Count == 0)
- return false;
-
- var prefix = article.Length == 0
- ? ""
- : (article.EndsWith("\n", StringComparison.Ordinal) ? "\n" : "\n\n");
-
- DraftArticle = article + prefix + string.Join("\n", links);
- return true;
- }
-
- private string ResolveUserFileUrl(string relativePath)
- {
- var authority = Settings?.Authentication?.Authority;
- if (!string.IsNullOrWhiteSpace(authority)
- && Uri.TryCreate(authority, UriKind.Absolute, out var baseUri))
- {
- return FileServerUrlHelpers.GetUserFilesUri(baseUri, relativePath).ToString();
- }
-
- return $"{Yavsc.Constants.UserFilesPath}/{relativePath}";
- }
-
- private static string EscapePathSegment(string segment)
- => Uri.EscapeDataString(segment);
}
diff --git a/src/PostIt/PostIt/ViewModels/ACL/PostAclDialogViewModel.cs b/src/PostIt/PostIt/ViewModels/PostAclDialogViewModel.cs
similarity index 100%
rename from src/PostIt/PostIt/ViewModels/ACL/PostAclDialogViewModel.cs
rename to src/PostIt/PostIt/ViewModels/PostAclDialogViewModel.cs
diff --git a/src/PostIt/PostIt/ViewModels/Activity/SelectableHairPrestationItem.cs b/src/PostIt/PostIt/ViewModels/SelectableHairPrestationItem.cs
similarity index 100%
rename from src/PostIt/PostIt/ViewModels/Activity/SelectableHairPrestationItem.cs
rename to src/PostIt/PostIt/ViewModels/SelectableHairPrestationItem.cs
diff --git a/src/PostIt/PostIt/ViewModels/Layout/SessionStatusViewModel.cs b/src/PostIt/PostIt/ViewModels/SessionStatusViewModel.cs
similarity index 100%
rename from src/PostIt/PostIt/ViewModels/Layout/SessionStatusViewModel.cs
rename to src/PostIt/PostIt/ViewModels/SessionStatusViewModel.cs
diff --git a/src/PostIt/PostIt/ViewModels/Settings/AuthenticationSettings.cs b/src/PostIt/PostIt/ViewModels/Settings/AuthenticationSettings.cs
index 3c23187e4..8034820d9 100644
--- a/src/PostIt/PostIt/ViewModels/Settings/AuthenticationSettings.cs
+++ b/src/PostIt/PostIt/ViewModels/Settings/AuthenticationSettings.cs
@@ -22,7 +22,7 @@ public partial class AuthenticationSettings : ObservableObject
public const string DefaultClientId = "postit";
- public static readonly string[] DefaultScopes = { "blogs", "api" };
+ public static readonly string[] DefaultScopes = { "blogs" };
[ObservableProperty]
public partial string Authority { get; set; }
diff --git a/src/PostIt/PostIt/ViewModels/Settings/Settings.cs b/src/PostIt/PostIt/ViewModels/Settings/Settings.cs
index 1c2802e33..0864c1416 100644
--- a/src/PostIt/PostIt/ViewModels/Settings/Settings.cs
+++ b/src/PostIt/PostIt/ViewModels/Settings/Settings.cs
@@ -15,8 +15,7 @@ namespace PostIt.ViewModels;
public partial class Settings : ViewModelBase
{
- [JsonIgnore]
- public string? SettingsFileFullName { get; private set; }
+ const string SettingsFileName = "postit-settings.json";
[ObservableProperty]
public partial AuthenticationSettings Authentication { get; set; } = new();
@@ -33,70 +32,10 @@ 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;
-
[ObservableProperty]
[JsonIgnore]
public partial StatusNotice ActionStatus { get; set; } = StatusNotice.Info("Pret.");
-
- public bool Loaded { get; private set; } = false;
-
-
- ///
- /// True when the in-memory state has drifted from the last
- /// or snapshot. The
- /// Settings page binds the Sauver button's IsEnabled to
- /// this flag, so it only enables when the user has actually
- /// touched something since the last load / save. Cleared by
- /// (and by ), set by
- /// every successful setter on the four top-level mutable
- /// properties and on the sub-properties of
- /// .
- ///
- [ObservableProperty]
- public partial bool IsDirty { get; private set; } = false;
-
-
- ///
- /// Guards every mutation of the observable state. [ObservableProperty]
- /// generates setters that call SetProperty(...) which fires
- /// PropertyChanged. Avalonia bindings consume that event on
- /// the UI thread, and a stray background-thread update is exactly
- /// what crashed DataValidationErrors.SetErrors on
- /// postit://callback re-launches. The lock makes mutations
- /// atomic;
- /// then marshals the notification onto the UI thread so bindings
- /// observe the change on the right thread.
- ///
- private readonly object _mutationGate = new();
-
- ///
- /// Scopes the PostIt client always requires from the OIDC provider,
- /// regardless of what the user has in their settings file.
- ///
- /// PostIt calls into the Blog API (and any other Yavsc API
- /// gated by an [Authorize("…Scope")] policy) and is silent
- /// about the contract: a missing scope here surfaces as a 401
- /// on the very first API call after login, with no obvious link
- /// to the settings. The "feature" scopes the user must opt into
- /// (e.g. blogs) are still their choice — we only force the
- /// structural ones that OIDC itself needs.
- ///
- private static readonly string[] BuiltInScopes = new[]
- {
- "openid", // OIDC: required for the id_token
- "profile", // OIDC: standard profile claims
- "offline_access", // OIDC: required to receive a refresh_token
- "blogs",
- "api"
- };
- private readonly string DEFAULT_SETTINGS_FILENAME = "postit-settings.json";
-
public void SetActionStatus(string message, StatusSeverity severity = StatusSeverity.Info)
{
ActionStatus = severity switch
@@ -123,7 +62,6 @@ public partial class Settings : ViewModelBase
partial void OnBlogsApiUrlChanged(string value) => MarkDirty();
partial void OnApiUrlChanged(string value) => MarkDirty();
partial void OnSearchTextChanged(string value) => MarkDirty();
- partial void OnProviderOngoingRequestsSortOptionChanged(string value) => MarkDirty();
///
/// Authentication can be reassigned wholesale by
@@ -144,6 +82,35 @@ public partial class Settings : ViewModelBase
MarkDirty();
}
+ public bool Loaded { get; private set; } = false;
+
+ ///
+ /// True when the in-memory state has drifted from the last
+ /// or snapshot. The
+ /// Settings page binds the Sauver button's IsEnabled to
+ /// this flag, so it only enables when the user has actually
+ /// touched something since the last load / save. Cleared by
+ /// (and by ), set by
+ /// every successful setter on the four top-level mutable
+ /// properties and on the sub-properties of
+ /// .
+ ///
+ [ObservableProperty]
+ public partial bool IsDirty { get; private set; } = false;
+
+ ///
+ /// Guards every mutation of the observable state. [ObservableProperty]
+ /// generates setters that call SetProperty(...) which fires
+ /// PropertyChanged. Avalonia bindings consume that event on
+ /// the UI thread, and a stray background-thread update is exactly
+ /// what crashed DataValidationErrors.SetErrors on
+ /// postit://callback re-launches. The lock makes mutations
+ /// atomic;
+ /// then marshals the notification onto the UI thread so bindings
+ /// observe the change on the right thread.
+ ///
+ private readonly object _mutationGate = new();
+
///
/// Build OidcClient options configured for Authorization Code + PKCE
/// (no client secret). The browser implementation should be supplied
@@ -207,6 +174,27 @@ public partial class Settings : ViewModelBase
Authentication.RefreshScopeListText();
}
+ ///
+ /// Scopes the PostIt client always requires from the OIDC provider,
+ /// regardless of what the user has in their settings file.
+ ///
+ /// PostIt calls into the Blog API (and any other Yavsc API
+ /// gated by an [Authorize("…Scope")] policy) and is silent
+ /// about the contract: a missing scope here surfaces as a 401
+ /// on the very first API call after login, with no obvious link
+ /// to the settings. The "feature" scopes the user must opt into
+ /// (e.g. blogs) are still their choice — we only force the
+ /// structural ones that OIDC itself needs.
+ ///
+ private static readonly string[] BuiltInScopes = new[]
+ {
+ "openid", // OIDC: required for the id_token
+ "profile", // OIDC: standard profile claims
+ "offline_access", // OIDC: required to receive a refresh_token
+ "blogs",
+ "api"
+
+ };
///
/// Merge user-configured scopes with the built-in ones. User scopes
@@ -259,42 +247,16 @@ public partial class Settings : ViewModelBase
return;
}
}
- if (Environment.GetEnvironmentVariable("POSTIT_SETTINGS_JSON") is string envJson
- && !string.IsNullOrWhiteSpace(envJson))
- {
- Console.WriteLine("🔎 Loading settings from POSTIT_SETTINGS_JSON environment variable.");
- FileInfo configByEnvFileInfo = new FileInfo(envJson);
- if (!configByEnvFileInfo.Exists)
- {
- throw new Exception($"🩎 Settings file not found at {configByEnvFileInfo.FullName}");
- }
- string json = File.ReadAllText(configByEnvFileInfo.FullName);
- ApplyJson(json, "POSTIT_SETTINGS_JSON");
- SettingsFileFullName = configByEnvFileInfo.FullName;
- Loaded = true;
- return;
- }
+
string configDir = Path.Combine(
- Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
- "PostIt"
- );
+ Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
+ "PostIt"
+);
+ Directory.CreateDirectory(configDir);
- if (SettingsFileFullName is not null)
- {
- // Already set by a previous Load() or by the environment
- // variable path above. Use it as-is.
- }
- else if (Environment.GetEnvironmentVariable("POSTIT_SETTINGS_JSON") is string envPath
- && !string.IsNullOrWhiteSpace(envPath))
- {
- SettingsFileFullName = envPath;
- }
- else
- {
- SettingsFileFullName = Path.Combine(configDir, "postit-settings.json");
- }
+ string configPath = Path.Combine(configDir, SettingsFileName);
- FileInfo configFileInfo = new FileInfo(SettingsFileFullName);
+ FileInfo configFileInfo = new FileInfo(configPath);
if (!configFileInfo.Exists)
{
@@ -323,7 +285,6 @@ public partial class Settings : ViewModelBase
using var reader = new StreamReader(stream);
var json = reader.ReadToEnd();
ApplyJson(json, $"user file {configFileInfo.FullName}");
- SettingsFileFullName = configFileInfo.FullName;
Loaded = true;
}
catch (Exception ex)
@@ -375,7 +336,7 @@ public partial class Settings : ViewModelBase
// → our overridden dispatcher-safe marshaller below.
else lock (_mutationGate)
{
- var legacyApiUrl = TryReadApiUrl(json);
+ var legacyApiUrl = TryReadLegacyApiUrl(json);
this.Authentication = settings.Authentication;
this.DarkMode = settings.DarkMode;
this.BlogsApiUrl = !string.IsNullOrWhiteSpace(settings.BlogsApiUrl)
@@ -385,7 +346,6 @@ public partial class Settings : ViewModelBase
? settings.ApiUrl
: this.ApiUrl;
this.SearchText = settings.SearchText ?? string.Empty;
- this.ProviderOngoingRequestsSortOption = settings.ProviderOngoingRequestsSortOption ?? string.Empty;
if (!(settings.Authentication is null))
{
this.Authentication = new AuthenticationSettings();
@@ -431,7 +391,7 @@ public partial class Settings : ViewModelBase
}
}
- private static string? TryReadApiUrl(string json)
+ private static string? TryReadLegacyApiUrl(string json)
{
try
{
@@ -464,7 +424,6 @@ public partial class Settings : ViewModelBase
this.BlogsApiUrl = "https://blogs.pschneider.fr/api/v1/";
this.ApiUrl = "https://api.pschneider.fr/api/v1/";
this.SearchText = string.Empty;
- this.ProviderOngoingRequestsSortOption = string.Empty;
}
///
@@ -487,17 +446,11 @@ public partial class Settings : ViewModelBase
{
SetActionStatus("Enregistrement des parametres...", StatusSeverity.Info);
- if (SettingsFileFullName is null)
- {
- var configDir = Path.Combine(
- Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
- "PostIt");
- Directory.CreateDirectory(configDir);
- SettingsFileFullName = Path.Combine(configDir, DEFAULT_SETTINGS_FILENAME);
- }
-
- var configPath = SettingsFileFullName!;
- Directory.CreateDirectory(Path.GetDirectoryName(configPath)!);
+ var configDir = Path.Combine(
+ Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
+ "PostIt");
+ Directory.CreateDirectory(configDir);
+ var configPath = Path.Combine(configDir, SettingsFileName);
lock (_mutationGate)
{
@@ -513,7 +466,7 @@ public partial class Settings : ViewModelBase
UnixFileMode.UserRead | UnixFileMode.UserWrite);
IsDirty = false;
SetActionStatus("Parametres sauvegardes.", StatusSeverity.Info);
-
+
Console.WriteLine($"💾 Settings saved to {configPath}");
}
catch (Exception ex)
diff --git a/src/PostIt/PostIt/ViewModels/Signature/SignaturePageViewModel.cs b/src/PostIt/PostIt/ViewModels/SignaturePageViewModel.cs
similarity index 100%
rename from src/PostIt/PostIt/ViewModels/Signature/SignaturePageViewModel.cs
rename to src/PostIt/PostIt/ViewModels/SignaturePageViewModel.cs
diff --git a/src/PostIt/PostIt/ViewModels/Layout/StatusNotice.cs b/src/PostIt/PostIt/ViewModels/StatusNotice.cs
similarity index 86%
rename from src/PostIt/PostIt/ViewModels/Layout/StatusNotice.cs
rename to src/PostIt/PostIt/ViewModels/StatusNotice.cs
index c87af390c..972c64350 100644
--- a/src/PostIt/PostIt/ViewModels/Layout/StatusNotice.cs
+++ b/src/PostIt/PostIt/ViewModels/StatusNotice.cs
@@ -23,8 +23,8 @@ public sealed class StatusNotice
(Glyph, Background, BorderBrush, Foreground) = severity switch
{
- StatusSeverity.Error => ("!", "#7F1D1D", "#C62828", "#e1f0f6"),
- StatusSeverity.Warning => ("~", "#7C4A03", "#E6A700", "#eaeaea"),
+ StatusSeverity.Error => ("!", "#FDECEA", "#C62828", "#7F1D1D"),
+ StatusSeverity.Warning => ("~", "#FFF8E1", "#E6A700", "#7C4A03"),
_ => ("i", "#E8F0FE", "#5B8DEF", "#1E3A8A"),
};
}
diff --git a/src/PostIt/PostIt/Views/Activity/ActivitiesPage.axaml b/src/PostIt/PostIt/Views/ActivitiesPage.axaml
similarity index 100%
rename from src/PostIt/PostIt/Views/Activity/ActivitiesPage.axaml
rename to src/PostIt/PostIt/Views/ActivitiesPage.axaml
diff --git a/src/PostIt/PostIt/Views/Activity/ActivitiesPage.axaml.cs b/src/PostIt/PostIt/Views/ActivitiesPage.axaml.cs
similarity index 100%
rename from src/PostIt/PostIt/Views/Activity/ActivitiesPage.axaml.cs
rename to src/PostIt/PostIt/Views/ActivitiesPage.axaml.cs
diff --git a/src/PostIt/PostIt/Views/Activity/EstimateEditionPage.axaml b/src/PostIt/PostIt/Views/Activity/EstimateEditionPage.axaml
deleted file mode 100644
index 7b25ab226..000000000
--- a/src/PostIt/PostIt/Views/Activity/EstimateEditionPage.axaml
+++ /dev/null
@@ -1,151 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/src/PostIt/PostIt/Views/Activity/EstimateEditionPage.axaml.cs b/src/PostIt/PostIt/Views/Activity/EstimateEditionPage.axaml.cs
deleted file mode 100644
index 1c58efa10..000000000
--- a/src/PostIt/PostIt/Views/Activity/EstimateEditionPage.axaml.cs
+++ /dev/null
@@ -1,17 +0,0 @@
-using Avalonia.Controls;
-using Avalonia.Markup.Xaml;
-
-namespace PostIt.Views;
-
-public partial class EstimateEditionPage : ContentPage
-{
- public EstimateEditionPage()
- {
- InitializeComponent();
- }
-
- private void InitializeComponent()
- {
- AvaloniaXamlLoader.Load(this);
- }
-}
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 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/src/PostIt/PostIt/Views/Activity/EstimateListPage.axaml.cs b/src/PostIt/PostIt/Views/Activity/EstimateListPage.axaml.cs
deleted file mode 100644
index 0eac3b66e..000000000
--- a/src/PostIt/PostIt/Views/Activity/EstimateListPage.axaml.cs
+++ /dev/null
@@ -1,17 +0,0 @@
-using Avalonia.Controls;
-using Avalonia.Markup.Xaml;
-
-namespace PostIt.Views;
-
-public partial class EstimateListPage : ContentPage
-{
- public EstimateListPage()
- {
- InitializeComponent();
- }
-
- private void InitializeComponent()
- {
- AvaloniaXamlLoader.Load(this);
- }
-}
diff --git a/src/PostIt/PostIt/Views/Activity/ProviderOngoingRequestsPage.axaml b/src/PostIt/PostIt/Views/Activity/ProviderOngoingRequestsPage.axaml
deleted file mode 100644
index 173fe903b..000000000
--- a/src/PostIt/PostIt/Views/Activity/ProviderOngoingRequestsPage.axaml
+++ /dev/null
@@ -1,102 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/src/PostIt/PostIt/Views/Activity/ProviderOngoingRequestsPage.axaml.cs b/src/PostIt/PostIt/Views/Activity/ProviderOngoingRequestsPage.axaml.cs
deleted file mode 100644
index ce2e9ad40..000000000
--- a/src/PostIt/PostIt/Views/Activity/ProviderOngoingRequestsPage.axaml.cs
+++ /dev/null
@@ -1,17 +0,0 @@
-using Avalonia.Controls;
-using Avalonia.Markup.Xaml;
-
-namespace PostIt.Views;
-
-public partial class ProviderOngoingRequestsPage : ContentPage
-{
- public ProviderOngoingRequestsPage()
- {
- InitializeComponent();
- }
-
- private void InitializeComponent()
- {
- AvaloniaXamlLoader.Load(this);
- }
-}
diff --git a/src/PostIt/PostIt/Views/ACL/AddCircleMemberDialog.axaml b/src/PostIt/PostIt/Views/AddCircleMemberDialog.axaml
similarity index 100%
rename from src/PostIt/PostIt/Views/ACL/AddCircleMemberDialog.axaml
rename to src/PostIt/PostIt/Views/AddCircleMemberDialog.axaml
diff --git a/src/PostIt/PostIt/Views/ACL/AddCircleMemberDialog.axaml.cs b/src/PostIt/PostIt/Views/AddCircleMemberDialog.axaml.cs
similarity index 100%
rename from src/PostIt/PostIt/Views/ACL/AddCircleMemberDialog.axaml.cs
rename to src/PostIt/PostIt/Views/AddCircleMemberDialog.axaml.cs
diff --git a/src/PostIt/PostIt/Views/Activity/BillingQueriesPage.axaml b/src/PostIt/PostIt/Views/BillingQueriesPage.axaml
similarity index 100%
rename from src/PostIt/PostIt/Views/Activity/BillingQueriesPage.axaml
rename to src/PostIt/PostIt/Views/BillingQueriesPage.axaml
diff --git a/src/PostIt/PostIt/Views/Activity/BillingQueriesPage.axaml.cs b/src/PostIt/PostIt/Views/BillingQueriesPage.axaml.cs
similarity index 100%
rename from src/PostIt/PostIt/Views/Activity/BillingQueriesPage.axaml.cs
rename to src/PostIt/PostIt/Views/BillingQueriesPage.axaml.cs
diff --git a/src/PostIt/PostIt/Views/Activity/BillingQueryDetailsPage.axaml b/src/PostIt/PostIt/Views/BillingQueryDetailsPage.axaml
similarity index 100%
rename from src/PostIt/PostIt/Views/Activity/BillingQueryDetailsPage.axaml
rename to src/PostIt/PostIt/Views/BillingQueryDetailsPage.axaml
diff --git a/src/PostIt/PostIt/Views/Activity/BillingQueryDetailsPage.axaml.cs b/src/PostIt/PostIt/Views/BillingQueryDetailsPage.axaml.cs
similarity index 100%
rename from src/PostIt/PostIt/Views/Activity/BillingQueryDetailsPage.axaml.cs
rename to src/PostIt/PostIt/Views/BillingQueryDetailsPage.axaml.cs
diff --git a/src/PostIt/PostIt/Views/Blogs/BlogsPage.axaml b/src/PostIt/PostIt/Views/Blogs/BlogsPage.axaml
deleted file mode 100644
index cb63ed6d3..000000000
--- a/src/PostIt/PostIt/Views/Blogs/BlogsPage.axaml
+++ /dev/null
@@ -1,161 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/src/PostIt/PostIt/Views/Blogs/BlogsPage.axaml.cs b/src/PostIt/PostIt/Views/Blogs/BlogsPage.axaml.cs
deleted file mode 100644
index 09ccc79f4..000000000
--- a/src/PostIt/PostIt/Views/Blogs/BlogsPage.axaml.cs
+++ /dev/null
@@ -1,71 +0,0 @@
-using System;
-using System.IO;
-using System.Linq;
-using Avalonia.Controls;
-using Avalonia.Platform.Storage;
-using Avalonia.Controls.Primitives;
-
-namespace PostIt.Views.Blogs;
-
-public partial class BlogsPage : ContentPage
-{
- public BlogsPage()
- {
- InitializeComponent();
- }
-
- protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
- {
- base.OnApplyTemplate(e);
- if (DataContext is ViewModels.BlogsViewModel vm)
- {
- if (!vm.IsLoaded)
- {
- vm.RefreshAsync().Wait();
- }
- }
- }
-
- private async void AddAttachment_Click(object? sender, Avalonia.Interactivity.RoutedEventArgs e)
- {
- var topLevel = TopLevel.GetTopLevel(this);
- if (topLevel is null || DataContext is not ViewModels.BlogsViewModel vm)
- return;
-
- var files = await topLevel.StorageProvider.OpenFilePickerAsync(new FilePickerOpenOptions
- {
- Title = "Choisir des fichiers à joindre au billet",
- AllowMultiple = true,
- });
-
- if (files.Count == 0)
- return;
-
- var uploads = new System.Collections.Generic.List(files.Count);
- foreach (var file in files)
- {
- await using var stream = await file.OpenReadAsync();
- using var memory = new MemoryStream();
- await stream.CopyToAsync(memory);
- uploads.Add(new Yavsc.Api.Client.BlogUploadFile(file.Name, memory.ToArray(), GetMimeType(file.Name)));
- }
-
- vm.DraftAttachments.Clear();
- foreach (var upload in uploads)
- vm.DraftAttachments.Add(upload);
- }
-
- private static string GetMimeType(string fileName)
- {
- var ext = Path.GetExtension(fileName)?.ToLowerInvariant();
- return ext switch
- {
- ".png" => "image/png",
- ".jpg" or ".jpeg" => "image/jpeg",
- ".webp" => "image/webp",
- ".gif" => "image/gif",
- ".pdf" => "application/pdf",
- _ => "application/octet-stream"
- };
- }
-}
diff --git a/src/PostIt/PostIt/Views/ACL/CirclesPage.axaml b/src/PostIt/PostIt/Views/CirclesPage.axaml
similarity index 100%
rename from src/PostIt/PostIt/Views/ACL/CirclesPage.axaml
rename to src/PostIt/PostIt/Views/CirclesPage.axaml
diff --git a/src/PostIt/PostIt/Views/ACL/CirclesPage.axaml.cs b/src/PostIt/PostIt/Views/CirclesPage.axaml.cs
similarity index 100%
rename from src/PostIt/PostIt/Views/ACL/CirclesPage.axaml.cs
rename to src/PostIt/PostIt/Views/CirclesPage.axaml.cs
diff --git a/src/PostIt/PostIt/Views/Activity/CommandFormsPage.axaml b/src/PostIt/PostIt/Views/CommandFormsPage.axaml
similarity index 91%
rename from src/PostIt/PostIt/Views/Activity/CommandFormsPage.axaml
rename to src/PostIt/PostIt/Views/CommandFormsPage.axaml
index 2565092e5..f44c098bd 100644
--- a/src/PostIt/PostIt/Views/Activity/CommandFormsPage.axaml
+++ b/src/PostIt/PostIt/Views/CommandFormsPage.axaml
@@ -34,13 +34,13 @@
diff --git a/src/PostIt/PostIt/Views/Activity/CommandFormsPage.axaml.cs b/src/PostIt/PostIt/Views/CommandFormsPage.axaml.cs
similarity index 100%
rename from src/PostIt/PostIt/Views/Activity/CommandFormsPage.axaml.cs
rename to src/PostIt/PostIt/Views/CommandFormsPage.axaml.cs
diff --git a/src/PostIt/PostIt/Views/Layout/HomePage.axaml b/src/PostIt/PostIt/Views/HomePage.axaml
similarity index 65%
rename from src/PostIt/PostIt/Views/Layout/HomePage.axaml
rename to src/PostIt/PostIt/Views/HomePage.axaml
index 3b62b891b..7f6527e3d 100644
--- a/src/PostIt/PostIt/Views/Layout/HomePage.axaml
+++ b/src/PostIt/PostIt/Views/HomePage.axaml
@@ -22,17 +22,5 @@
Command="{Binding OpenActivities}"
HorizontalAlignment="Center"
IsEnabled="{Binding SessionStatus.IsLoggedIn}"/>
-
-
-
diff --git a/src/PostIt/PostIt/Views/Layout/HomePage.axaml.cs b/src/PostIt/PostIt/Views/HomePage.axaml.cs
similarity index 100%
rename from src/PostIt/PostIt/Views/Layout/HomePage.axaml.cs
rename to src/PostIt/PostIt/Views/HomePage.axaml.cs
diff --git a/src/PostIt/PostIt/Views/MainPage.axaml b/src/PostIt/PostIt/Views/MainPage.axaml
new file mode 100644
index 000000000..871a2f541
--- /dev/null
+++ b/src/PostIt/PostIt/Views/MainPage.axaml
@@ -0,0 +1,114 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/PostIt/PostIt/Views/MainPage.axaml.cs b/src/PostIt/PostIt/Views/MainPage.axaml.cs
new file mode 100644
index 000000000..97e3f9227
--- /dev/null
+++ b/src/PostIt/PostIt/Views/MainPage.axaml.cs
@@ -0,0 +1,25 @@
+using System;
+using Avalonia.Controls;
+using Avalonia.Controls.Primitives;
+
+namespace PostIt.Views;
+
+public partial class MainPage : ContentPage
+{
+ public MainPage()
+ {
+ InitializeComponent();
+ }
+
+ protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
+ {
+ base.OnApplyTemplate(e);
+ if (DataContext is ViewModels.MainViewModel vm)
+ {
+ if (!vm.IsLoaded)
+ {
+ vm.RefreshAsync().Wait();
+ }
+ }
+ }
+}
diff --git a/src/PostIt/PostIt/Views/Layout/MainView.axaml b/src/PostIt/PostIt/Views/MainView.axaml
similarity index 92%
rename from src/PostIt/PostIt/Views/Layout/MainView.axaml
rename to src/PostIt/PostIt/Views/MainView.axaml
index 92f53eb54..643c0f208 100644
--- a/src/PostIt/PostIt/Views/Layout/MainView.axaml
+++ b/src/PostIt/PostIt/Views/MainView.axaml
@@ -6,11 +6,11 @@
xmlns:views="using:PostIt.Views"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="PostIt.Views.MainView"
- x:DataType="vm:HomePageViewModel">
+ x:DataType="vm:MainViewModel">
-
+
diff --git a/src/PostIt/PostIt/Views/Layout/MainView.axaml.cs b/src/PostIt/PostIt/Views/MainView.axaml.cs
similarity index 87%
rename from src/PostIt/PostIt/Views/Layout/MainView.axaml.cs
rename to src/PostIt/PostIt/Views/MainView.axaml.cs
index 22bf537c6..654b90e2d 100644
--- a/src/PostIt/PostIt/Views/Layout/MainView.axaml.cs
+++ b/src/PostIt/PostIt/Views/MainView.axaml.cs
@@ -13,7 +13,7 @@ public partial class MainView : UserControl
protected override void OnDataContextChanged(EventArgs e)
{
base.OnDataContextChanged(e);
- if (DataContext is ViewModels.BlogsViewModel vm)
+ if (DataContext is ViewModels.MainViewModel vm)
{
if (!vm.IsLoaded)
{
diff --git a/src/PostIt/PostIt/Views/Layout/MainWindow.axaml b/src/PostIt/PostIt/Views/MainWindow.axaml
similarity index 100%
rename from src/PostIt/PostIt/Views/Layout/MainWindow.axaml
rename to src/PostIt/PostIt/Views/MainWindow.axaml
diff --git a/src/PostIt/PostIt/Views/Layout/MainWindow.axaml.cs b/src/PostIt/PostIt/Views/MainWindow.axaml.cs
similarity index 100%
rename from src/PostIt/PostIt/Views/Layout/MainWindow.axaml.cs
rename to src/PostIt/PostIt/Views/MainWindow.axaml.cs
diff --git a/src/PostIt/PostIt/Views/ACL/PostAclDialog.axaml b/src/PostIt/PostIt/Views/PostAclDialog.axaml
similarity index 100%
rename from src/PostIt/PostIt/Views/ACL/PostAclDialog.axaml
rename to src/PostIt/PostIt/Views/PostAclDialog.axaml
diff --git a/src/PostIt/PostIt/Views/ACL/PostAclDialog.axaml.cs b/src/PostIt/PostIt/Views/PostAclDialog.axaml.cs
similarity index 100%
rename from src/PostIt/PostIt/Views/ACL/PostAclDialog.axaml.cs
rename to src/PostIt/PostIt/Views/PostAclDialog.axaml.cs
diff --git a/src/PostIt/PostIt/Views/Layout/SessionStatusBanner.axaml b/src/PostIt/PostIt/Views/SessionStatusBanner.axaml
similarity index 100%
rename from src/PostIt/PostIt/Views/Layout/SessionStatusBanner.axaml
rename to src/PostIt/PostIt/Views/SessionStatusBanner.axaml
diff --git a/src/PostIt/PostIt/Views/Layout/SessionStatusBanner.axaml.cs b/src/PostIt/PostIt/Views/SessionStatusBanner.axaml.cs
similarity index 100%
rename from src/PostIt/PostIt/Views/Layout/SessionStatusBanner.axaml.cs
rename to src/PostIt/PostIt/Views/SessionStatusBanner.axaml.cs
diff --git a/src/PostIt/PostIt/Views/Layout/SettingsPage.axaml b/src/PostIt/PostIt/Views/SettingsPage.axaml
similarity index 100%
rename from src/PostIt/PostIt/Views/Layout/SettingsPage.axaml
rename to src/PostIt/PostIt/Views/SettingsPage.axaml
diff --git a/src/PostIt/PostIt/Views/Layout/SettingsPage.axaml.cs b/src/PostIt/PostIt/Views/SettingsPage.axaml.cs
similarity index 100%
rename from src/PostIt/PostIt/Views/Layout/SettingsPage.axaml.cs
rename to src/PostIt/PostIt/Views/SettingsPage.axaml.cs
diff --git a/src/PostIt/PostIt/Views/Signature/SignaturePage.axaml b/src/PostIt/PostIt/Views/SignaturePage.axaml
similarity index 100%
rename from src/PostIt/PostIt/Views/Signature/SignaturePage.axaml
rename to src/PostIt/PostIt/Views/SignaturePage.axaml
diff --git a/src/PostIt/PostIt/Views/Signature/SignaturePage.axaml.cs b/src/PostIt/PostIt/Views/SignaturePage.axaml.cs
similarity index 100%
rename from src/PostIt/PostIt/Views/Signature/SignaturePage.axaml.cs
rename to src/PostIt/PostIt/Views/SignaturePage.axaml.cs
diff --git a/src/PostIt/PostIt/postit-settings sample.json b/src/PostIt/PostIt/postit-settings sample.json
new file mode 100644
index 000000000..e23839b1f
--- /dev/null
+++ b/src/PostIt/PostIt/postit-settings sample.json
@@ -0,0 +1,15 @@
+{
+ "Authentication": {
+ "ClientId": "postit",
+ "Authority": "https://yavsc.pschneider.fr"
+ },
+ "RedirectUri": "postit://callback",
+ "DarkMode": false,
+ "ApiUrl": "https://api.pschneider.fr/api/v1/",
+ "Scopes": [
+ "openid",
+ "profile",
+ "offline_access",
+ "blogs"
+ ]
+}
diff --git a/src/PostIt/PostIt/postit-settings-sample.json b/src/PostIt/PostIt/postit-settings-sample.json
deleted file mode 100644
index fbf1f68a0..000000000
--- a/src/PostIt/PostIt/postit-settings-sample.json
+++ /dev/null
@@ -1,17 +0,0 @@
-{
- "Authentication": {
- "ClientId": "postit",
- "Authority": "https://yavsc.pschneider.fr",
- "Scopes": [
- "openid",
- "profile",
- "offline_access",
- "blogs",
- "api"
- ],
- "RedirectUri": "postit://callback"
- },
- "DarkMode": false,
- "ApiUrl": "https://api.pschneider.fr/api/v1/"
-
-}
diff --git a/src/PostIt/PostIt/postit-settings.json b/src/PostIt/PostIt/postit-settings.json
index f62a9ac1c..080fed178 100644
--- a/src/PostIt/PostIt/postit-settings.json
+++ b/src/PostIt/PostIt/postit-settings.json
@@ -1,21 +1,15 @@
{
- "Authentication": {
- "Authority": "https://localhost:5001",
- "ClientId": "postit",
+ "Authentication": {
+ "ClientId": "postit",
+ "Authority": "https://yavsc.pschneider.fr/"
+ },
+ "RedirectUri": "postit://callback",
+ "DarkMode": true,
+ "ApiUrl": "https://api.pschneider.fr/api/v1/",
"Scopes": [
- "blogs",
- "api"
- ],
- "RedirectUri": "postit://callback"
- },
- "DarkMode": true,
- "BlogsApiUrl": "https://localhost:5003/api/v1/",
- "ApiUrl": "https://localhost:5005/api/v1/",
- "SearchText": "",
- "ProviderOngoingRequestsSortOption": "",
- "Loaded": true,
- "IsDirty": true,
- "CanNavigateNext": false,
- "CanNavigatePrevious": true,
- "SaveCommand": {}
-}
\ No newline at end of file
+ "openid",
+ "profile",
+ "offline_access",
+ "blogs"
+ ]
+}
diff --git a/src/Yavsc.Abstract/FileSystem/AbstractFileSystemHelpers.cs b/src/Yavsc.Abstract/FileSystem/AbstractFileSystemHelpers.cs
index 7665df8ab..14bc86435 100644
--- a/src/Yavsc.Abstract/FileSystem/AbstractFileSystemHelpers.cs
+++ b/src/Yavsc.Abstract/FileSystem/AbstractFileSystemHelpers.cs
@@ -7,6 +7,8 @@ namespace Yavsc.Server.Helpers
{
public static class AbstractFileSystemHelpers
{
+ public static string UserBillsDirName { set; get; }
+ public static string UserFilesDirName { set; get; }
///
/// Is Valid this Path?
@@ -62,6 +64,17 @@ namespace Yavsc.Server.Helpers
return sb.ToString();
}
+ public static UserDirectoryInfo GetUserFiles(string userId, string subdir)
+ {
+ UserDirectoryInfo di = new UserDirectoryInfo(UserFilesDirName, userId, subdir);
+ return di;
+ }
+ public static bool IsRegularFile(string userName, string subdir)
+ {
+ FileInfo fi = new FileInfo( Path.Combine(Path.Combine(UserFilesDirName, userName), subdir));
+ return fi.Exists;
+ }
+
// Server side only supports POSIX file systems
public const char RemoteDirectorySeparator = '/';
diff --git a/src/Yavsc.Abstract/Files/FileServerUrlHelpers.cs b/src/Yavsc.Abstract/Files/FileServerUrlHelpers.cs
deleted file mode 100644
index 64bcb405b..000000000
--- a/src/Yavsc.Abstract/Files/FileServerUrlHelpers.cs
+++ /dev/null
@@ -1,67 +0,0 @@
-namespace Yavsc.Abstract.Files;
-
-///
-/// Helpers pour dériver les URL publiques des fichiers statiques à partir
-/// d'une URL d'autorité OIDC ou d'un autre point d'entrée racine.
-///
-public static class FileServerUrlHelpers
-{
- ///
- /// Dérive la racine publique des fichiers utilisateur en alignant
- /// le chemin sur .
- ///
- ///
- /// URL absolue de base, typiquement l'autorité OIDC de Yavsc.Org.
- ///
- /// Une URL absolue pointant vers la racine des fichiers utilisateur.
- public static Uri GetUserFilesBaseUri(Uri authorityBaseUrl)
- {
- ArgumentNullException.ThrowIfNull(authorityBaseUrl);
-
- if (!authorityBaseUrl.IsAbsoluteUri)
- {
- throw new ArgumentException(
- "The authority base URL must be absolute.",
- nameof(authorityBaseUrl));
- }
-
- var baseString = authorityBaseUrl.GetLeftPart(UriPartial.Authority);
- return new Uri(new Uri(baseString, UriKind.Absolute), EnsureTrailingSlash(Yavsc.Constants.UserFilesPath));
- }
-
- ///
- /// Dérive la racine publique des fichiers utilisateur en alignant
- /// le chemin sur .
- ///
- ///
- /// URL absolue de base, typiquement l'autorité OIDC de Yavsc.Org.
- ///
- /// Une URL absolue pointant vers la racine des fichiers utilisateur.
- public static Uri GetUserFilesBaseUri(string authorityBaseUrl)
- => GetUserFilesBaseUri(new Uri(authorityBaseUrl, UriKind.Absolute));
-
- ///
- /// Construit l'URL d'un fichier utilisateur à partir de la base d'autorité
- /// et d'un chemin relatif sous la racine des fichiers.
- ///
- public static Uri GetUserFilesUri(Uri authorityBaseUrl, string relativePath)
- {
- ArgumentException.ThrowIfNullOrWhiteSpace(relativePath);
-
- var baseUri = GetUserFilesBaseUri(authorityBaseUrl);
- return new Uri(baseUri, NormalizeRelativePath(relativePath));
- }
-
- ///
- /// Construit l'URL d'un fichier utilisateur à partir de la base d'autorité
- /// et d'un chemin relatif sous la racine des fichiers.
- ///
- public static Uri GetUserFilesUri(string authorityBaseUrl, string relativePath)
- => GetUserFilesUri(new Uri(authorityBaseUrl, UriKind.Absolute), relativePath);
-
- private static string EnsureTrailingSlash(string path)
- => path.EndsWith("/", StringComparison.Ordinal) ? path : path + "/";
-
- private static string NormalizeRelativePath(string relativePath)
- => relativePath.TrimStart('/');
-}
diff --git a/src/Yavsc.Abstract/IT/CodeFromChars.cs b/src/Yavsc.Abstract/IT/CodeFromChars.cs
index e0e31ce28..ff441568e 100644
--- a/src/Yavsc.Abstract/IT/CodeFromChars.cs
+++ b/src/Yavsc.Abstract/IT/CodeFromChars.cs
@@ -44,6 +44,7 @@ namespace Yavsc.Abstract.IT
public bool Validate()
{
// this is a n*n task
+
throw new NotImplementedException();
}
diff --git a/src/Yavsc.Abstract/Identity/Security/FileAccessControlRulePayload.cs b/src/Yavsc.Abstract/Identity/Security/FileAccessControlRulePayload.cs
deleted file mode 100644
index af3af22bc..000000000
--- a/src/Yavsc.Abstract/Identity/Security/FileAccessControlRulePayload.cs
+++ /dev/null
@@ -1,12 +0,0 @@
-
-namespace Yavsc.Models.Access
-{
- using Yavsc.Abstract.Identity.Security;
-
- public class FileAccessControlRulePayload : CircleAuthorization
- {
- public virtual string Path { get; set; }
-
-
- }
-}
diff --git a/src/Yavsc.Abstract/Yavsc.Abstract.csproj b/src/Yavsc.Abstract/Yavsc.Abstract.csproj
index 4df6c53c2..78ffe531e 100644
--- a/src/Yavsc.Abstract/Yavsc.Abstract.csproj
+++ b/src/Yavsc.Abstract/Yavsc.Abstract.csproj
@@ -11,7 +11,7 @@
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
\ No newline at end of file
diff --git a/src/Yavsc.Api.Client/BillingApiClient.cs b/src/Yavsc.Api.Client/BillingApiClient.cs
index a9e9deda6..c3a0be103 100644
--- a/src/Yavsc.Api.Client/BillingApiClient.cs
+++ b/src/Yavsc.Api.Client/BillingApiClient.cs
@@ -77,14 +77,6 @@ public sealed class BillingApiClient
return items;
}
- public Task> GetProviderOngoingQueriesAsync(CancellationToken ct = default)
- {
- return _api.CallAsync>(
- HttpMethod.Get,
- Absolute("bill/provider/ongoing"),
- ct: ct);
- }
-
public async Task GetQueryAsync(string billingCode, long queryId, CancellationToken ct = default)
{
if (string.IsNullOrWhiteSpace(billingCode))
@@ -164,7 +156,6 @@ public sealed class BillingApiClient
? null
: new BillingLocationDto
{
- Id = dto.Location.Id > 0 ? dto.Location.Id : null,
Address = dto.Location.Address ?? string.Empty,
Latitude = dto.Location.Latitude,
Longitude = dto.Location.Longitude,
@@ -192,7 +183,6 @@ public sealed class BillingApiClient
? null
: new BillingLocationDto
{
- Id = dto.Location.Id > 0 ? dto.Location.Id : null,
Address = dto.Location.Address ?? string.Empty,
Latitude = dto.Location.Latitude,
Longitude = dto.Location.Longitude,
@@ -222,7 +212,6 @@ public sealed class BillingApiClient
? null
: new BillingLocationDto
{
- Id = dto.Location.Id > 0 ? dto.Location.Id : null,
Address = dto.Location.Address ?? string.Empty,
Latitude = dto.Location.Latitude,
Longitude = dto.Location.Longitude,
@@ -316,11 +305,6 @@ public sealed class BillingApiClient
["Address"] = location.Address,
};
- if (location.Id.HasValue && location.Id.Value > 0)
- {
- payload["Id"] = location.Id.Value;
- }
-
if (location.Latitude.HasValue)
{
payload["Latitude"] = location.Latitude.Value;
@@ -336,7 +320,6 @@ public sealed class BillingApiClient
private sealed class BillingLocationResponse
{
- public long Id { get; set; }
public string? Address { get; set; }
public double Latitude { get; set; }
public double Longitude { get; set; }
diff --git a/src/Yavsc.Api.Client/BlogApiClient.cs b/src/Yavsc.Api.Client/BlogApiClient.cs
index 48704dcb9..8284c5ed9 100644
--- a/src/Yavsc.Api.Client/BlogApiClient.cs
+++ b/src/Yavsc.Api.Client/BlogApiClient.cs
@@ -1,5 +1,8 @@
-using System.Net.Http.Headers;
-using System.Text.Json;
+using System;
+using System.Collections.Generic;
+using System.Net.Http;
+using System.Threading;
+using System.Threading.Tasks;
using Yavsc.Blogspot;
namespace Yavsc.Api.Client;
@@ -59,18 +62,11 @@ public sealed class BlogApiClient
public Task GetPostAsync(long id, CancellationToken ct = default)
=> _api.CallAsync(HttpMethod.Get, $"{_pathPrefix}/{id}", ct: ct);
- public Task CreatePostAsync(
- BlogPostDto post,
- IReadOnlyCollection? files = null,
- CancellationToken ct = default)
- => SendPostAsync(HttpMethod.Post, _pathPrefix, post, files, ct);
+ public Task CreatePostAsync(BlogPostDto post, CancellationToken ct = default)
+ => _api.CallAsync(HttpMethod.Post, _pathPrefix, body: post, ct: ct);
- public Task UpdatePostAsync(
- long id,
- BlogPostDto post,
- IReadOnlyCollection? files = null,
- CancellationToken ct = default)
- => SendPostAsync(HttpMethod.Put, $"{_pathPrefix}/{id}", post, files, ct);
+ public Task UpdatePostAsync(long id, BlogPostDto post, CancellationToken ct = default)
+ => _api.CallAsync(HttpMethod.Put, $"{_pathPrefix}/{id}", body: post, ct: ct);
public Task DeletePostAsync(long id, CancellationToken ct = default)
=> _api.CallAsync(HttpMethod.Delete, $"{_pathPrefix}/{id}", ct: ct);
@@ -85,50 +81,4 @@ public sealed class BlogApiClient
public Task SetPublishAsync(long id, bool publish, CancellationToken ct = default)
=> _api.CallAsync(HttpMethod.Put, $"{_pathPrefix}/{id}/publish",
body: new { publish }, ct: ct);
-
- private async Task SendPostAsync(
- HttpMethod method,
- string path,
- BlogPostDto post,
- IReadOnlyCollection? files,
- CancellationToken ct)
- {
- if (files is null || files.Count == 0)
- return await _api.CallAsync(method, path, body: post, ct: ct);
-
- return await _api.CallAsync(method, path, () => CreateMultipartContent(post, files), ct: ct);
- }
-
- /// Build the multipart/form-data body PostIt
- /// sends when a blog post is saved with one or more
- /// attachments: a blog field holding the
- /// serialised camelCase with
- /// WhenWritingNull, plus one file part per entry
- /// in (each carrying its
- /// , falling back to
- /// application/octet-stream when unset). Exposed as
- /// public so the blogs backend tests can assert on the
- /// wire shape directly and round-trip it through the live
- /// BlogsWebServerFixture host.
- public static HttpContent CreateMultipartContent(BlogPostDto post, IReadOnlyCollection files)
- {
- var content = new MultipartFormDataContent();
- var blogJson = JsonSerializer.Serialize(post, new JsonSerializerOptions
- {
- PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
- DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull,
- });
-
- content.Add(new StringContent(blogJson), "blog");
-
- foreach (var file in files)
- {
- var fileContent = new ByteArrayContent(file.Content);
- fileContent.Headers.ContentType = new MediaTypeHeaderValue(
- string.IsNullOrWhiteSpace(file.ContentType) ? "application/octet-stream" : file.ContentType);
- content.Add(fileContent, "file", file.FileName);
- }
-
- return content;
- }
}
diff --git a/src/Yavsc.Api.Client/BlogUploadFile.cs b/src/Yavsc.Api.Client/BlogUploadFile.cs
deleted file mode 100644
index 4b3a7b26d..000000000
--- a/src/Yavsc.Api.Client/BlogUploadFile.cs
+++ /dev/null
@@ -1,6 +0,0 @@
-namespace Yavsc.Api.Client;
-
-///
-/// Buffered file payload for multipart blog uploads.
-///
-public sealed record BlogUploadFile(string FileName, byte[] Content, string? ContentType = null);
diff --git a/src/Yavsc.Api.Client/Dtos/BillingQueryDetailsDto.cs b/src/Yavsc.Api.Client/Dtos/BillingQueryDetailsDto.cs
index eb33ba816..6d658ce49 100644
--- a/src/Yavsc.Api.Client/Dtos/BillingQueryDetailsDto.cs
+++ b/src/Yavsc.Api.Client/Dtos/BillingQueryDetailsDto.cs
@@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
-using System.Text.Json.Serialization;
using Yavsc;
namespace Yavsc.Api.Client;
@@ -30,14 +29,7 @@ public sealed class BillingQueryDetailsDto
public sealed class BillingLocationDto
{
- [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
- public long? Id { get; set; }
-
public string Address { get; set; } = string.Empty;
-
- [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public double? Latitude { get; set; }
-
- [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public double? Longitude { get; set; }
}
diff --git a/src/Yavsc.Api.Client/Dtos/EstimateDtos.cs b/src/Yavsc.Api.Client/Dtos/EstimateDtos.cs
deleted file mode 100644
index 43c09d94a..000000000
--- a/src/Yavsc.Api.Client/Dtos/EstimateDtos.cs
+++ /dev/null
@@ -1,52 +0,0 @@
-using System;
-using System.Collections.Generic;
-
-namespace Yavsc.Api.Client;
-
-///
-/// A single billable line of an estimate, mirroring the JSON shape of
-/// the server-side Yavsc.Models.Billing.CommandLine entity.
-///
-public sealed class EstimateLineDto
-{
- public long Id { get; set; }
- public string Name { get; set; } = string.Empty;
- public string Description { get; set; } = string.Empty;
- public int Count { get; set; } = 1;
- public decimal UnitaryCost { get; set; }
- public long EstimateId { get; set; }
- public string Currency { get; set; } = "EUR";
-}
-
-///
-/// Estimate payload exchanged with the api/v1/estimate routes
-/// (EstimateApiController). and
-/// are always initialised: the server-side
-/// entity reads them from non-nullable string properties and a null
-/// list would break its serialisation.
-///
-public sealed class EstimateDto
-{
- public long Id { get; set; }
- public long? CommandId { get; set; }
- public string Title { get; set; } = string.Empty;
- public string Description { get; set; } = string.Empty;
- public List Bill { get; set; } = new();
- public List AttachedGraphics { get; set; } = new();
- public List AttachedFiles { get; set; } = new();
- public string? OwnerId { get; set; }
- public string ClientId { get; set; } = string.Empty;
- public string CommandType { get; set; } = string.Empty;
- public DateTime ProviderValidationDate { get; set; }
- public DateTime ClientValidationDate { get; set; }
-}
-
-///
-/// Response of a successful estimate creation
-/// (Ok(new { estimate.Id, estimate.Bill })).
-///
-public sealed class EstimateCreatedDto
-{
- public long Id { get; set; }
- public List Bill { get; set; } = new();
-}
diff --git a/src/Yavsc.Api.Client/EstimateApiClient.cs b/src/Yavsc.Api.Client/EstimateApiClient.cs
deleted file mode 100644
index 4a80bbe62..000000000
--- a/src/Yavsc.Api.Client/EstimateApiClient.cs
+++ /dev/null
@@ -1,126 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Net.Http;
-using System.Threading;
-using System.Threading.Tasks;
-
-namespace Yavsc.Api.Client;
-
-///
-/// HTTP client for the estimate routes (api/v1/estimate,
-/// served by EstimateApiController). Follows the same
-/// DTO↔path mapper shape as : all
-/// transport concerns (base URL, JSON, Bearer auth, silent refresh
-/// on 401) are delegated to .
-///
-public sealed class EstimateApiClient
-{
- private const string PathPrefix = "estimate";
-
- private readonly IYavscApiClient _api;
- private readonly Func _businessBaseAddress;
-
- public EstimateApiClient(IYavscApiClient api, string businessBaseAddress)
- : this(api, () => businessBaseAddress)
- {
- }
-
- public EstimateApiClient(IYavscApiClient api, Func businessBaseAddress)
- {
- _api = api ?? throw new ArgumentNullException(nameof(api));
- _businessBaseAddress = businessBaseAddress ?? throw new ArgumentNullException(nameof(businessBaseAddress));
-
- // Validate initial value early to fail fast on invalid setup.
- _ = ResolveBusinessBaseAddress();
- }
-
- ///
- /// Lists the estimates of the given owner; when
- /// is null, the server falls back to the current user.
- ///
- public Task> GetEstimatesAsync(string? ownerId = null, CancellationToken ct = default)
- {
- var path = string.IsNullOrWhiteSpace(ownerId)
- ? PathPrefix
- : $"{PathPrefix}?ownerId={Uri.EscapeDataString(ownerId)}";
-
- return _api.CallAsync>(HttpMethod.Get, Absolute(path), ct: ct);
- }
-
- public Task GetEstimateAsync(long id, CancellationToken ct = default)
- {
- if (id <= 0)
- throw new ArgumentOutOfRangeException(nameof(id));
-
- return _api.CallAsync(HttpMethod.Get, Absolute($"{PathPrefix}/{id}"), ct: ct);
- }
-
- ///
- /// Lists the ongoing estimates (not yet validated by the client) where
- /// the current user is the client who initiated the request
- /// (GET estimate/asclient).
- ///
- public Task> GetOngoingEstimatesAsClientAsync(CancellationToken ct = default)
- => _api.CallAsync>(HttpMethod.Get, Absolute($"{PathPrefix}/asclient"), ct: ct);
-
- ///
- /// Lists the ongoing estimates (not yet validated by the client)
- /// established by the current user as the provider
- /// (GET estimate/asprovider).
- ///
- public Task> GetOngoingEstimatesAsProviderAsync(CancellationToken ct = default)
- => _api.CallAsync>(HttpMethod.Get, Absolute($"{PathPrefix}/asprovider"), ct: ct);
-
- ///
- /// Creates an estimate. When is set,
- /// the server also stamps the linked command as validated.
- ///
- public async Task CreateAsync(EstimateDto estimate, CancellationToken ct = default)
- {
- if (estimate is null)
- throw new ArgumentNullException(nameof(estimate));
-
- var created = await _api.CallAsync(
- HttpMethod.Post,
- Absolute(PathPrefix),
- body: estimate,
- ct: ct).ConfigureAwait(false);
-
- return created ?? new EstimateCreatedDto();
- }
-
- public Task UpdateAsync(long id, EstimateDto estimate, CancellationToken ct = default)
- {
- if (id <= 0)
- throw new ArgumentOutOfRangeException(nameof(id));
- if (estimate is null)
- throw new ArgumentNullException(nameof(estimate));
-
- estimate.Id = id;
-
- return _api.CallAsync(
- HttpMethod.Put,
- Absolute($"{PathPrefix}/{id}"),
- body: estimate,
- ct: ct);
- }
-
- public Task DeleteAsync(long id, CancellationToken ct = default)
- {
- if (id <= 0)
- throw new ArgumentOutOfRangeException(nameof(id));
-
- return _api.CallAsync(HttpMethod.Delete, Absolute($"{PathPrefix}/{id}"), ct: ct);
- }
-
- 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);
- }
-}
diff --git a/src/Yavsc.Api.Client/IYavscApiClient.cs b/src/Yavsc.Api.Client/IYavscApiClient.cs
index 1d470b682..209ec07d7 100644
--- a/src/Yavsc.Api.Client/IYavscApiClient.cs
+++ b/src/Yavsc.Api.Client/IYavscApiClient.cs
@@ -53,24 +53,10 @@ public interface IYavscApiClient : IAsyncDisposable
object? body = null,
CancellationToken ct = default);
- /// Call a multipart endpoint with a typed return value.
- Task CallAsync(
- HttpMethod method,
- string path,
- Func contentFactory,
- CancellationToken ct = default);
-
/// Call a JSON endpoint that returns no useful body (DELETE, 204, etc.).
Task CallAsync(
HttpMethod method,
string path,
object? body = null,
CancellationToken ct = default);
-
- /// Call a multipart endpoint that returns no useful body.
- Task CallAsync(
- HttpMethod method,
- string path,
- Func contentFactory,
- CancellationToken ct = default);
}
diff --git a/src/Yavsc.Api.Client/Yavsc.Api.Client.csproj b/src/Yavsc.Api.Client/Yavsc.Api.Client.csproj
index f0cdb55c9..6ceaab113 100644
--- a/src/Yavsc.Api.Client/Yavsc.Api.Client.csproj
+++ b/src/Yavsc.Api.Client/Yavsc.Api.Client.csproj
@@ -17,8 +17,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/Yavsc.Api.Test/BillingControllerTests.cs b/src/Yavsc.Api.Test/BillingControllerTests.cs
deleted file mode 100644
index 38b9a9919..000000000
--- a/src/Yavsc.Api.Test/BillingControllerTests.cs
+++ /dev/null
@@ -1,236 +0,0 @@
-using System.Net;
-using System.Net.Http.Headers;
-using System.Net.Http.Json;
-using Microsoft.EntityFrameworkCore;
-using Microsoft.Extensions.DependencyInjection;
-using Yavsc.Api.Test.Fixtures;
-using Yavsc.Helpers;
-using Yavsc.Models;
-using Yavsc.Models.Billing;
-using Yavsc.Models.Haircut;
-using Yavsc.Models.Workflow;
-using Yavsc.Tests.Shared;
-
-namespace Yavsc.Api.Test;
-
-[Collection("Yavsc Api")]
-public sealed class BillingControllerTests : IClassFixture
-{
- private readonly ApiWebServerFixture _fixture;
-
- public BillingControllerTests(ApiWebServerFixture fixture)
- {
- _fixture = fixture;
- }
-
- private HttpClient NewClient(string subject = "alice", string scope = "api")
- {
- var handler = new HttpClientHandler
- {
- ServerCertificateCustomValidationCallback = (_, _, _, _) => true
- };
-
- var http = new HttpClient(handler)
- {
- BaseAddress = new Uri(_fixture.BaseAddress)
- };
-
- http.DefaultRequestHeaders.Authorization =
- new AuthenticationHeaderValue("Bearer", TestTokenIssuer.Issue(subject, scope));
-
- return http;
- }
-
- [Fact]
- public async Task GetProviderOngoingCommands_returns_current_provider_requests()
- {
- WorkflowHelpers.ConfigureBillingService();
- _fixture.ResetAndSeedActivityGraph();
-
- using (var scope = _fixture.Services.CreateScope())
- {
- var db = scope.ServiceProvider.GetRequiredService();
- var location = db.Locations.Single();
-
- db.RdvQueries.Add(new RdvQuery
- {
- ActivityCode = "dev",
- ClientId = "bob",
- PerformerId = "alice",
- Consent = true,
- UserCreated = "alice",
- UserModified = "alice",
- DateCreated = DateTime.UtcNow.AddMinutes(-10),
- DateModified = DateTime.UtcNow.AddMinutes(-8),
- EventDate = DateTime.UtcNow.AddDays(1),
- Location = location,
- Reason = "Rendez-vous fournisseur",
- Status = QueryStatus.InProgress,
- Description = "Commande fournisseur en cours",
- });
-
- db.RdvQueries.Add(new RdvQuery
- {
- ActivityCode = "dev",
- ClientId = "alice",
- PerformerId = "bob",
- Consent = true,
- UserCreated = "bob",
- UserModified = "bob",
- DateCreated = DateTime.UtcNow.AddMinutes(-20),
- DateModified = DateTime.UtcNow.AddMinutes(-20),
- EventDate = DateTime.UtcNow.AddDays(2),
- Location = location,
- Reason = "Commande d'un autre prestataire",
- Status = QueryStatus.Accepted,
- Description = "Autre prestataire",
- });
-
- db.SaveChanges();
- }
-
- using var http = NewClient();
-
- var response = await http.GetAsync("/api/v1/bill/provider/ongoing", TestContext.Current.CancellationToken);
- var body = await response.Content.ReadAsStringAsync(TestContext.Current.CancellationToken);
-
- Assert.True(response.StatusCode == HttpStatusCode.OK, $"Unexpected status {(int)response.StatusCode} ({response.StatusCode}): {body}");
-
- var payload = await response.Content.ReadFromJsonAsync>(TestContext.Current.CancellationToken);
- Assert.NotNull(payload);
- Assert.NotEmpty(payload!);
- Assert.All(payload!, item => Assert.Equal("alice", item.PerformerId));
- Assert.Contains(payload!, item => item.BillingCode == BillingCodes.Rdv);
- }
-
- [Fact]
- public async Task GetProviderOngoingCommands_ignores_rows_with_invalid_discriminator()
- {
- WorkflowHelpers.ConfigureBillingService();
- _fixture.ResetAndSeedActivityGraph();
-
- using (var scope = _fixture.Services.CreateScope())
- {
- var db = scope.ServiceProvider.GetRequiredService();
-
- db.Database.ExecuteSqlInterpolated($@"
-INSERT INTO ""NominativeServiceCommands""
-(""ActivityCode"", ""ClientId"", ""Consent"", ""DateCreated"", ""DateModified"", ""Description"", ""Discriminator"", ""PerformerId"", ""Status"", ""UserCreated"", ""UserModified"")
-VALUES
-({"dev"}, {"bob"}, {true}, {DateTime.UtcNow.AddMinutes(-5)}, {DateTime.UtcNow.AddMinutes(-4)}, {"Legacy malformed row"}, {""}, {"alice"}, {(int)QueryStatus.Accepted}, {"alice"}, {"alice"});
-");
-
- var location = db.Locations.Single();
- db.RdvQueries.Add(new RdvQuery
- {
- ActivityCode = "dev",
- ClientId = "bob",
- PerformerId = "alice",
- Consent = true,
- UserCreated = "alice",
- UserModified = "alice",
- DateCreated = DateTime.UtcNow.AddMinutes(-3),
- DateModified = DateTime.UtcNow.AddMinutes(-2),
- EventDate = DateTime.UtcNow.AddDays(1),
- Location = location,
- Reason = "Commande valide",
- Status = QueryStatus.InProgress,
- Description = "Commande fournisseur valide",
- });
-
- db.SaveChanges();
- }
-
- using var http = NewClient();
-
- var response = await http.GetAsync("/api/v1/bill/provider/ongoing", TestContext.Current.CancellationToken);
- var body = await response.Content.ReadAsStringAsync(TestContext.Current.CancellationToken);
-
- Assert.True(response.StatusCode == HttpStatusCode.OK, $"Unexpected status {(int)response.StatusCode} ({response.StatusCode}): {body}");
-
- var payload = await response.Content.ReadFromJsonAsync>(TestContext.Current.CancellationToken);
- Assert.NotNull(payload);
- Assert.NotEmpty(payload!);
- Assert.Contains(payload!, item => item.BillingCode == BillingCodes.Rdv && item.PerformerId == "alice");
- Assert.DoesNotContain(payload!, item => string.IsNullOrWhiteSpace(item.BillingCode));
- }
-
- [Fact]
- public async Task GetProviderOngoingCommands_returns_haircut_and_grouped_haircut_requests()
- {
- WorkflowHelpers.ConfigureBillingService();
- _fixture.ResetAndSeedHaircutGraph();
-
- using (var scope = _fixture.Services.CreateScope())
- {
- var db = scope.ServiceProvider.GetRequiredService();
- db.UserActivities.Add(new UserActivity
- {
- UserId = "alice",
- DoesCode = "brush",
- Weight = 50,
- });
- db.UserActivities.Add(new UserActivity
- {
- UserId = "alice",
- DoesCode = "mbrush",
- Weight = 50,
- });
- db.CommandForm.Add(new CommandForm
- {
- ActivityCode = "brush",
- ActionName = BillingCodes.Brush,
- Title = "Brush",
- });
- db.CommandForm.Add(new CommandForm
- {
- ActivityCode = "mbrush",
- ActionName = BillingCodes.MBrush,
- Title = "MBrush",
- });
- db.SaveChanges();
- }
-
- using var http = NewClient();
-
- var response = await http.GetAsync("/api/v1/bill/provider/ongoing", TestContext.Current.CancellationToken);
- var body = await response.Content.ReadAsStringAsync(TestContext.Current.CancellationToken);
-
- Assert.True(response.StatusCode == HttpStatusCode.OK, $"Unexpected status {(int)response.StatusCode} ({response.StatusCode}): {body}");
-
- var payload = await response.Content.ReadFromJsonAsync>(TestContext.Current.CancellationToken);
- Assert.NotNull(payload);
- Assert.Contains(payload!, item => item.BillingCode == BillingCodes.Brush && item.PerformerId == "alice");
- Assert.Contains(payload!, item => item.BillingCode == BillingCodes.MBrush && item.PerformerId == "alice");
- }
-
- [Fact]
- public async Task GetProviderOngoingCommands_excludes_requests_outside_performer_declared_activities()
- {
- WorkflowHelpers.ConfigureBillingService();
- _fixture.ResetAndSeedHaircutGraph();
-
- using var http = NewClient();
-
- var response = await http.GetAsync("/api/v1/bill/provider/ongoing", TestContext.Current.CancellationToken);
- var body = await response.Content.ReadAsStringAsync(TestContext.Current.CancellationToken);
-
- Assert.True(response.StatusCode == HttpStatusCode.OK, $"Unexpected status {(int)response.StatusCode} ({response.StatusCode}): {body}");
-
- var payload = await response.Content.ReadFromJsonAsync>(TestContext.Current.CancellationToken);
- Assert.NotNull(payload);
- Assert.DoesNotContain(payload!, item => item.BillingCode == BillingCodes.Brush);
- Assert.DoesNotContain(payload!, item => item.BillingCode == BillingCodes.MBrush);
- }
-
- private sealed class ProviderOngoingCommandDto
- {
- public long Id { get; set; }
- public string BillingCode { get; set; } = string.Empty;
- public string ActivityCode { get; set; } = string.Empty;
- public string PerformerId { get; set; } = string.Empty;
- public string ClientId { get; set; } = string.Empty;
- public QueryStatus Status { get; set; }
- public string Description { get; set; } = string.Empty;
- }
-}
diff --git a/src/Yavsc.Api.Test/EstimateApiControllerTests.cs b/src/Yavsc.Api.Test/EstimateApiControllerTests.cs
deleted file mode 100644
index cb945f51a..000000000
--- a/src/Yavsc.Api.Test/EstimateApiControllerTests.cs
+++ /dev/null
@@ -1,315 +0,0 @@
-using System.Net;
-using System.Net.Http.Headers;
-using System.Net.Http.Json;
-using System.Text.Json;
-using Microsoft.EntityFrameworkCore;
-using Microsoft.Extensions.DependencyInjection;
-using Yavsc.Api.Test.Fixtures;
-using Yavsc.Models;
-using Yavsc.Models.Billing;
-using Yavsc.Models.Workflow;
-using Yavsc.Tests.Shared;
-
-namespace Yavsc.Api.Test;
-
-[Collection("Yavsc Api")]
-public sealed class EstimateApiControllerTests : IClassFixture
-{
- private readonly ApiWebServerFixture _fixture;
-
- public EstimateApiControllerTests(ApiWebServerFixture fixture)
- {
- _fixture = fixture;
- }
-
- private HttpClient NewClient(string subject = "alice", string scope = "api")
- {
- var handler = new HttpClientHandler
- {
- ServerCertificateCustomValidationCallback = (_, _, _, _) => true
- };
-
- var http = new HttpClient(handler)
- {
- BaseAddress = new Uri(_fixture.BaseAddress)
- };
-
- http.DefaultRequestHeaders.Authorization =
- new AuthenticationHeaderValue("Bearer", TestTokenIssuer.Issue(subject, scope));
-
- return http;
- }
-
- ///
- /// Seed a provider (alice) and a client (bob) with a pending
- /// from bob to alice, and return the
- /// command id.
- ///
- private long SeedPendingCommand()
- {
- _fixture.ResetAndSeedActivityGraph();
-
- using var scope = _fixture.Services.CreateScope();
- var db = scope.ServiceProvider.GetRequiredService();
- var location = db.Locations.Single();
-
- var query = new RdvQuery
- {
- ActivityCode = "dev",
- ClientId = "bob",
- PerformerId = "alice",
- Consent = true,
- UserCreated = "bob",
- UserModified = "bob",
- DateCreated = DateTime.UtcNow.AddMinutes(-10),
- DateModified = DateTime.UtcNow.AddMinutes(-10),
- EventDate = DateTime.UtcNow.AddDays(1),
- Location = location,
- Reason = "Demande de devis",
- Status = QueryStatus.InProgress,
- Description = "Demande en attente de devis",
- };
- db.RdvQueries.Add(query);
- db.SaveChanges();
-
- return query.Id;
- }
-
- private static object NewEstimatePayload(long? commandId, string clientId, string? ownerId = null)
- => new
- {
- CommandId = commandId,
- ClientId = clientId,
- OwnerId = ownerId,
- CommandType = BillingCodes.Rdv,
- Title = "Devis prestation",
- Description = "Devis détaillé",
- AttachedFiles = Array.Empty(),
- AttachedGraphics = Array.Empty(),
- Bill = new[]
- {
- new { Name = "Prestation", Description = "Prestation de base", Count = 1, UnitaryCost = 120m, Currency = "EUR" },
- new { Name = "Remise", Description = "Remise fidélité", Count = 1, UnitaryCost = -20m, Currency = "EUR" },
- },
- };
-
- [Fact]
- public async Task PostEstimate_creates_the_estimate_and_validates_the_linked_command()
- {
- var commandId = SeedPendingCommand();
- using var http = NewClient("alice");
-
- var response = await http.PostAsJsonAsync(
- "/api/v1/estimate",
- NewEstimatePayload(commandId, clientId: "bob"),
- TestContext.Current.CancellationToken);
- var body = await response.Content.ReadAsStringAsync(TestContext.Current.CancellationToken);
-
- Assert.True(response.StatusCode == HttpStatusCode.OK, $"Unexpected status {(int)response.StatusCode} ({response.StatusCode}): {body}");
-
- using var doc = JsonDocument.Parse(body);
- var estimateId = doc.RootElement.GetProperty("id").GetInt64();
- Assert.True(estimateId > 0);
- Assert.Equal(2, doc.RootElement.GetProperty("bill").GetArrayLength());
-
- using var scope = _fixture.Services.CreateScope();
- var db = scope.ServiceProvider.GetRequiredService();
-
- var estimate = db.Estimates.Include(e => e.Bill).Single(e => e.Id == estimateId);
- Assert.Equal("alice", estimate.OwnerId);
- Assert.Equal("bob", estimate.ClientId);
- Assert.Equal(commandId, estimate.CommandId);
- Assert.Equal(BillingCodes.Rdv, estimate.CommandType);
- Assert.Equal(2, estimate.Bill.Count);
- Assert.Contains(estimate.Bill, line => line.UnitaryCost == -20m);
-
- // PostEstimate stamps the linked command as validated.
- var query = db.RdvQueries.Single(q => q.Id == commandId);
- Assert.NotNull(query.ValidationDate);
- }
-
- [Fact]
- public async Task PostEstimate_without_command_creates_a_standalone_estimate()
- {
- _fixture.ResetAndSeedActivityGraph();
- using var http = NewClient("alice");
-
- var response = await http.PostAsJsonAsync(
- "/api/v1/estimate",
- NewEstimatePayload(commandId: null, clientId: "bob"),
- TestContext.Current.CancellationToken);
- var body = await response.Content.ReadAsStringAsync(TestContext.Current.CancellationToken);
-
- Assert.True(response.StatusCode == HttpStatusCode.OK, $"Unexpected status {(int)response.StatusCode} ({response.StatusCode}): {body}");
-
- using var doc = JsonDocument.Parse(body);
- var estimateId = doc.RootElement.GetProperty("id").GetInt64();
-
- using var scope = _fixture.Services.CreateScope();
- var db = scope.ServiceProvider.GetRequiredService();
- var estimate = db.Estimates.Single(e => e.Id == estimateId);
- Assert.Null(estimate.CommandId);
- Assert.Equal("alice", estimate.OwnerId);
- }
-
- [Fact]
- public async Task PostEstimate_for_another_owner_is_rejected()
- {
- _fixture.ResetAndSeedActivityGraph();
- using var http = NewClient("alice");
-
- var response = await http.PostAsJsonAsync(
- "/api/v1/estimate",
- NewEstimatePayload(commandId: null, clientId: "bob", ownerId: "bob"),
- TestContext.Current.CancellationToken);
-
- Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
-
- using var scope = _fixture.Services.CreateScope();
- var db = scope.ServiceProvider.GetRequiredService();
- Assert.Empty(db.Estimates);
- }
-
- [Fact]
- public async Task PostEstimate_with_an_unknown_command_id_is_rejected()
- {
- _fixture.ResetAndSeedActivityGraph();
- using var http = NewClient("alice");
-
- var response = await http.PostAsJsonAsync(
- "/api/v1/estimate",
- NewEstimatePayload(commandId: 999999, clientId: "bob"),
- TestContext.Current.CancellationToken);
-
- Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
-
- using var scope = _fixture.Services.CreateScope();
- var db = scope.ServiceProvider.GetRequiredService();
- Assert.Empty(db.Estimates);
- }
-
- [Fact]
- public async Task PostEstimate_without_token_is_unauthorized()
- {
- _fixture.ResetAndSeedActivityGraph();
-
- var handler = new HttpClientHandler
- {
- ServerCertificateCustomValidationCallback = (_, _, _, _) => true
- };
- using var http = new HttpClient(handler) { BaseAddress = new Uri(_fixture.BaseAddress) };
-
- var response = await http.PostAsJsonAsync(
- "/api/v1/estimate",
- NewEstimatePayload(commandId: null, clientId: "bob"),
- TestContext.Current.CancellationToken);
-
- Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
- }
-
- ///
- /// Seeds an estimate owned by alice (the provider) for the given
- /// client, optionally already validated by the client.
- ///
- private long SeedEstimate(string clientId, bool clientValidated)
- {
- using var scope = _fixture.Services.CreateScope();
- var db = scope.ServiceProvider.GetRequiredService();
-
- var estimate = new Estimate
- {
- ClientId = clientId,
- OwnerId = "alice",
- CommandType = BillingCodes.Rdv,
- Title = "Devis prestation",
- Description = "Devis de test",
- AttachedFiles = new List(),
- AttachedGraphics = new List(),
- Bill = new List
- {
- new() { Name = "Prestation", Description = "Prestation de base", Count = 1, UnitaryCost = 120m, Currency = "EUR" },
- },
- ClientValidationDate = clientValidated ? DateTime.UtcNow : default,
- };
- db.Estimates.Add(estimate);
- db.SaveChanges();
- return estimate.Id;
- }
-
- [Fact]
- public async Task GetOngoingEstimatesAsClient_returns_only_the_current_users_ongoing_estimates()
- {
- _fixture.ResetAndSeedActivityGraph();
- var ongoingId = SeedEstimate(clientId: "bob", clientValidated: false);
- SeedEstimate(clientId: "bob", clientValidated: true); // already validated: excluded
- SeedEstimate(clientId: "alice", clientValidated: false); // another client's: excluded
-
- using var http = NewClient("bob");
-
- var response = await http.GetAsync("/api/v1/estimate/asclient", TestContext.Current.CancellationToken);
- var body = await response.Content.ReadAsStringAsync(TestContext.Current.CancellationToken);
-
- Assert.True(response.StatusCode == HttpStatusCode.OK, $"Unexpected status {(int)response.StatusCode} ({response.StatusCode}): {body}");
-
- using var doc = JsonDocument.Parse(body);
- var items = doc.RootElement.EnumerateArray().ToArray();
- var item = Assert.Single(items);
- Assert.Equal(ongoingId, item.GetProperty("id").GetInt64());
- Assert.Equal("bob", item.GetProperty("clientId").GetString());
- Assert.Equal("alice", item.GetProperty("ownerId").GetString());
- }
-
- [Fact]
- public async Task GetOngoingEstimatesAsProvider_returns_only_ongoing_estimates_established_by_the_provider()
- {
- _fixture.ResetAndSeedActivityGraph();
- var ongoingId = SeedEstimate(clientId: "bob", clientValidated: false);
- SeedEstimate(clientId: "bob", clientValidated: true); // already validated: excluded
-
- using var http = NewClient("alice");
-
- var response = await http.GetAsync("/api/v1/estimate/asprovider", TestContext.Current.CancellationToken);
- var body = await response.Content.ReadAsStringAsync(TestContext.Current.CancellationToken);
-
- Assert.True(response.StatusCode == HttpStatusCode.OK, $"Unexpected status {(int)response.StatusCode} ({response.StatusCode}): {body}");
-
- using var doc = JsonDocument.Parse(body);
- var items = doc.RootElement.EnumerateArray().ToArray();
- var item = Assert.Single(items);
- Assert.Equal(ongoingId, item.GetProperty("id").GetInt64());
- Assert.Equal("alice", item.GetProperty("ownerId").GetString());
- Assert.Single(item.GetProperty("bill").EnumerateArray());
- }
-
- [Fact]
- public async Task GetOngoingEstimatesAsClient_without_token_is_unauthorized()
- {
- _fixture.ResetAndSeedActivityGraph();
-
- var handler = new HttpClientHandler
- {
- ServerCertificateCustomValidationCallback = (_, _, _, _) => true
- };
- using var http = new HttpClient(handler) { BaseAddress = new Uri(_fixture.BaseAddress) };
-
- var response = await http.GetAsync("/api/v1/estimate/asclient", TestContext.Current.CancellationToken);
-
- Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
- }
-
- [Fact]
- public async Task GetOngoingEstimatesAsProvider_without_token_is_unauthorized()
- {
- _fixture.ResetAndSeedActivityGraph();
-
- var handler = new HttpClientHandler
- {
- ServerCertificateCustomValidationCallback = (_, _, _, _) => true
- };
- using var http = new HttpClient(handler) { BaseAddress = new Uri(_fixture.BaseAddress) };
-
- var response = await http.GetAsync("/api/v1/estimate/asprovider", TestContext.Current.CancellationToken);
-
- Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
- }
-}
diff --git a/src/Yavsc.Api.Test/Fixtures/ApiWebServerFixture.cs b/src/Yavsc.Api.Test/Fixtures/ApiWebServerFixture.cs
index 51e939ceb..e2a8b3376 100644
--- a/src/Yavsc.Api.Test/Fixtures/ApiWebServerFixture.cs
+++ b/src/Yavsc.Api.Test/Fixtures/ApiWebServerFixture.cs
@@ -1,72 +1,45 @@
+using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Builder;
using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;
-using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.IdentityModel.Tokens;
-using Npgsql;
using Yavsc.Controllers;
-using Yavsc.Interfaces.Workflow;
using Yavsc.Models;
-using Yavsc.Models.Billing;
-using Yavsc.Models.Google.Messaging;
using Yavsc.Models.Haircut;
-using Yavsc.Models.Messaging;
using Yavsc.Models.Relationship;
using Yavsc.Models.Workflow;
-using Yavsc.Services;
using Yavsc.Tests.Shared;
namespace Yavsc.Api.Test.Fixtures;
public sealed class ApiWebServerFixture : WebHostFixture
{
- private const string DbProviderEnvVar = "YAVSC_API_TEST_DB_PROVIDER";
- private const string NpgsqlAdminConnectionEnvVar = "YAVSC_API_TEST_NPGSQL_ADMIN_CONNECTION";
- private const string DedicatedNpgsqlDatabaseName = "yavscTestDb";
- private const string DefaultDevelopmentConnectionString = "Server=localhost;Port=5432;Database=yavscdev;Username=yavscdev;Password=8*5idas;Include Error Detail=true";
-
protected override int HttpsPort => 5104;
private static SqliteConnection? _sharedSqliteConnection;
private static readonly object _sqliteLock = new();
- private static readonly object _npgsqlLock = new();
- private static string? _sharedNpgsqlConnectionString;
protected override WebApplication BuildApp(WebApplicationBuilder builder)
{
- if (UseNpgsqlProvider())
+ SqliteConnection sharedConnection;
+ lock (_sqliteLock)
{
- var npgsqlConnectionString = EnsureNpgsqlDatabaseCreated();
- builder.Services.AddDbContext(opt =>
- opt.UseNpgsql(npgsqlConnectionString,
- x => x.MigrationsAssembly("Yavsc.Org")));
- }
- else
- {
- SqliteConnection sharedConnection;
- lock (_sqliteLock)
+ if (_sharedSqliteConnection is null)
{
- if (_sharedSqliteConnection is null)
- {
- _sharedSqliteConnection = new SqliteConnection(
- "Data Source=YavscApiTests;Mode=Memory;Cache=Shared");
- _sharedSqliteConnection.Open();
- }
- sharedConnection = _sharedSqliteConnection;
+ _sharedSqliteConnection = new SqliteConnection(
+ "Data Source=YavscApiTests;Mode=Memory;Cache=Shared");
+ _sharedSqliteConnection.Open();
}
-
- builder.Services.AddDbContext(opt =>
- opt.UseSqlite(sharedConnection));
+ sharedConnection = _sharedSqliteConnection;
}
+ builder.Services.AddDbContext(opt =>
+ opt.UseSqlite(sharedConnection));
+
builder.Services.AddControllers()
.AddApplicationPart(typeof(ActivityApiController).Assembly);
- builder.Services.AddLocalization();
- builder.Services.Configure(_ => { });
- builder.Services.AddTransient();
- builder.Services.AddTransient();
builder.Services.AddAuthorization();
builder.Services.AddAuthentication("Bearer")
@@ -101,18 +74,7 @@ public sealed class ApiWebServerFixture : WebHostFixture
using (var scope = app.Services.CreateScope())
{
var db = scope.ServiceProvider.GetRequiredService();
- if (UseNpgsqlProvider())
- {
- // Apply the EF Core migrations (Yavsc.Org assembly) so the
- // test database schema matches the production provider.
- // EnsureCreated must not be used here: it would create the
- // schema without the migrations history and break Migrate().
- db.Database.Migrate();
- }
- else
- {
- db.Database.EnsureCreated();
- }
+ db.Database.EnsureCreated();
}
await Task.CompletedTask;
@@ -121,97 +83,12 @@ public sealed class ApiWebServerFixture : WebHostFixture
public string BaseAddress => Addresses.First(a => a.StartsWith("https://", StringComparison.Ordinal));
- public static bool UseNpgsqlProvider()
- => string.Equals(
- Environment.GetEnvironmentVariable(DbProviderEnvVar),
- "npgsql",
- StringComparison.OrdinalIgnoreCase);
-
- private static string EnsureNpgsqlDatabaseCreated()
- {
- lock (_npgsqlLock)
- {
- if (!string.IsNullOrWhiteSpace(_sharedNpgsqlConnectionString))
- {
- return _sharedNpgsqlConnectionString;
- }
-
- var adminConnectionString = BuildAdminConnectionString();
- var databaseName = DedicatedNpgsqlDatabaseName;
-
- using (var adminConnection = new NpgsqlConnection(adminConnectionString))
- {
- adminConnection.Open();
- using var existsCommand = adminConnection.CreateCommand();
- existsCommand.CommandText = "SELECT 1 FROM pg_database WHERE datname = @databaseName";
- existsCommand.Parameters.AddWithValue("databaseName", databaseName);
-
- if (existsCommand.ExecuteScalar() is null)
- {
- using var createCommand = adminConnection.CreateCommand();
- createCommand.CommandText = $"CREATE DATABASE \"{databaseName}\"";
- createCommand.ExecuteNonQuery();
- }
- }
-
- var testConnectionBuilder = new NpgsqlConnectionStringBuilder(adminConnectionString)
- {
- Database = databaseName,
- Pooling = false,
- IncludeErrorDetail = true
- };
-
- _sharedNpgsqlConnectionString = testConnectionBuilder.ToString();
- return _sharedNpgsqlConnectionString;
- }
- }
-
- private static string BuildAdminConnectionString()
- {
- var configured = Environment.GetEnvironmentVariable(NpgsqlAdminConnectionEnvVar);
- var source = string.IsNullOrWhiteSpace(configured)
- ? DefaultDevelopmentConnectionString
- : configured;
-
- var builder = new NpgsqlConnectionStringBuilder(source)
- {
- Pooling = false,
- IncludeErrorDetail = true
- };
-
- if (string.IsNullOrWhiteSpace(configured))
- {
- builder.Database = "postgres";
- }
- else if (string.IsNullOrWhiteSpace(builder.Database))
- {
- builder.Database = "postgres";
- }
-
- return builder.ToString();
- }
-
- private sealed class NoopMessageSender : IYavscMessageSender
- {
- public Task NotifyBookQueryAsync(IEnumerable connectionIds, RdvQueryEvent ev)
- => Task.FromResult(new MessageWithPayloadResponse());
-
- public Task NotifyEstimateAsync(IEnumerable connectionIds, EstimationEvent ev)
- => Task.FromResult(new MessageWithPayloadResponse());
-
- public Task NotifyHairCutQueryAsync(IEnumerable connectionIds, HairCutQueryEvent ev)
- => Task.FromResult(new MessageWithPayloadResponse());
-
- public Task NotifyAsync(IEnumerable connectionIds, IEvent yaev)
- => Task.FromResult(new MessageWithPayloadResponse());
- }
-
public void ResetAndSeedActivityGraph()
{
using var scope = Services.CreateScope();
var db = scope.ServiceProvider.GetRequiredService();
- ResetDatabase(db);
+ db.Database.EnsureDeleted();
db.Database.EnsureCreated();
var user = new ApplicationUser
@@ -312,34 +189,6 @@ public sealed class ApiWebServerFixture : WebHostFixture
db.SaveChanges();
}
- private static void ResetDatabase(ApplicationDbContext db)
- {
- if (UseNpgsqlProvider())
- {
- // Purge only the tables of the test graph, children before
- // parents, so no DELETE violates a foreign key. Estimate and
- // CommandLine reference NominativeServiceCommand (CommandId) and
- // must be deleted before the Rdv/HairCut/HairMultiCut queries.
- db.Set().RemoveRange(db.Set());
- db.Set().RemoveRange(db.Set());
- db.Set().RemoveRange(db.Set());
- db.Set().RemoveRange(db.Set());
- db.Set().RemoveRange(db.Set());
- db.Set().RemoveRange(db.Set());
- db.Set().RemoveRange(db.Set());
- db.Set().RemoveRange(db.Set());
- db.Set().RemoveRange(db.Set());
- db.Set().RemoveRange(db.Set());
- db.Set().RemoveRange(db.Set());
- db.Set().RemoveRange(db.Set());
-
- db.SaveChanges();
- return;
- }
-
- db.Database.EnsureDeleted();
- }
-
public void ResetAndSeedRdvQueryGraph()
{
ResetAndSeedActivityGraph();
@@ -486,9 +335,21 @@ public sealed class ApiWebServerFixture : WebHostFixture
public override void Dispose()
{
- // Keep the shared in-memory SQLite connection alive for the
- // whole test process. Closing it from one fixture instance can
- // drop the schema while other tests are still running.
- base.Dispose();
+ try
+ {
+ base.Dispose();
+ }
+ finally
+ {
+ lock (_sqliteLock)
+ {
+ if (_sharedSqliteConnection is not null)
+ {
+ _sharedSqliteConnection.Close();
+ _sharedSqliteConnection.Dispose();
+ _sharedSqliteConnection = null;
+ }
+ }
+ }
}
}
diff --git a/src/Yavsc.Api.Test/RdvQueryApiControllerTests.cs b/src/Yavsc.Api.Test/RdvQueryApiControllerTests.cs
index c3e4aad2b..668814239 100644
--- a/src/Yavsc.Api.Test/RdvQueryApiControllerTests.cs
+++ b/src/Yavsc.Api.Test/RdvQueryApiControllerTests.cs
@@ -143,87 +143,4 @@ public sealed class RdvQueryApiControllerTests : IClassFixture(TestContext.Current.CancellationToken);
- Assert.NotNull(created);
- Assert.NotNull(created!.Location);
- Assert.True(created.Location.Id > 0);
- Assert.NotEqual(999999L, created.Location.Id);
- Assert.Equal("alice", created.ClientId);
- }
-
- [Fact]
- public async Task PostQuery_without_location_returns_bad_request()
- {
- _fixture.ResetAndSeedRdvQueryGraph();
- using var http = NewClient(subject: "alice");
-
- var createPayload = new
- {
- ActivityCode = "dev",
- PerformerId = "alice",
- Consent = true,
- EventDate = DateTime.UtcNow.AddDays(1),
- Reason = "Rendez-vous sans location",
- Status = QueryStatus.Inserted,
- };
-
- var createResponse = await http.PostAsJsonAsync("/api/v1/billing/Rdv", createPayload, TestContext.Current.CancellationToken);
- Assert.Equal(HttpStatusCode.BadRequest, createResponse.StatusCode);
- }
-
- [Fact]
- public async Task PostQuery_with_unknown_location_id_and_missing_address_returns_bad_request()
- {
- _fixture.ResetAndSeedRdvQueryGraph();
- using var http = NewClient(subject: "alice");
-
- var createPayload = new
- {
- ActivityCode = "dev",
- PerformerId = "alice",
- Consent = true,
- EventDate = DateTime.UtcNow.AddDays(1),
- Location = new
- {
- Id = 777777L,
- Address = "",
- Latitude = 0.0,
- Longitude = 0.0,
- },
- Reason = "Rendez-vous location invalide",
- Status = QueryStatus.Inserted,
- };
-
- var createResponse = await http.PostAsJsonAsync("/api/v1/billing/Rdv", createPayload, TestContext.Current.CancellationToken);
- Assert.Equal(HttpStatusCode.BadRequest, createResponse.StatusCode);
- }
}
diff --git a/src/Yavsc.Api.Test/Yavsc.Api.Test.csproj b/src/Yavsc.Api.Test/Yavsc.Api.Test.csproj
index ebe96dfa8..760f5ce84 100644
--- a/src/Yavsc.Api.Test/Yavsc.Api.Test.csproj
+++ b/src/Yavsc.Api.Test/Yavsc.Api.Test.csproj
@@ -6,11 +6,8 @@
false
Yavsc.Api.Test
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
+
@@ -21,6 +18,7 @@
+
@@ -28,7 +26,8 @@
+
-
\ No newline at end of file
+
diff --git a/src/Yavsc.Api/Controllers/Business/BillingController.cs b/src/Yavsc.Api/Controllers/Business/BillingController.cs
index ba099e7c7..197e06b11 100644
--- a/src/Yavsc.Api/Controllers/Business/BillingController.cs
+++ b/src/Yavsc.Api/Controllers/Business/BillingController.cs
@@ -3,12 +3,9 @@ using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Localization;
using Newtonsoft.Json;
using System.Security.Claims;
-using Yavsc.Billing;
using Yavsc.Helpers;
using Yavsc.ViewModels;
using Yavsc.Models.Billing;
-using Yavsc.Models.Haircut;
-using Yavsc.Models.Workflow;
using Yavsc.Server.Models.FileSystem;
namespace Yavsc.ApiControllers
@@ -35,7 +32,6 @@ namespace Yavsc.ApiControllers
private readonly ILogger logger;
private readonly IBillingService billingService;
- private readonly SiteSettings siteSettings;
public BillingController(
IAuthorizationService authorizationService,
@@ -44,8 +40,7 @@ namespace Yavsc.ApiControllers
ApplicationDbContext context,
IOptions googleSettings,
IYavscMessageSender GCMSender,
- IBillingService billingService,
- IOptions siteSettings
+ IBillingService billingService
)
{
_googleSettings=googleSettings.Value;
@@ -55,7 +50,6 @@ namespace Yavsc.ApiControllers
this._localizer = SR;
_GCMSender=GCMSender;
this.billingService=billingService;
- this.siteSettings = siteSettings.Value;
}
[HttpGet("facture-{billingCode}-{id}.pdf"), Authorize]
@@ -68,7 +62,7 @@ namespace Yavsc.ApiControllers
return new ChallengeResult();
}
- var fi = bill.GetBillInfo(billingService, siteSettings);
+ var fi = bill.GetBillInfo(billingService);
if (!fi.Exists) return Ok(new { Error = "Not generated" });
return File(fi.OpenRead(), "application/x-pdf", fi.Name);
@@ -106,114 +100,6 @@ namespace Yavsc.ApiControllers
return ViewComponent("Bill",new object[] { billingCode, bill, OutputFormat.Pdf, true } );
}
- ///
- /// Lists ongoing service commands for the authenticated performer.
- /// This endpoint is tailored for the PostIt provider homepage flow
- /// ("Mes demandes en cours").
- ///
- [HttpGet("provider/ongoing")]
- [Produces("application/json")]
- public IActionResult GetProviderOngoingCommands()
- {
- var uid = User.GetUserId();
- if (string.IsNullOrWhiteSpace(uid))
- {
- return Unauthorized();
- }
-
- if (billingService.BillingMap.Count == 0)
- {
- WorkflowHelpers.ConfigureBillingService();
- }
-
- var allowedActivityCodes = dbContext.UserActivities
- .AsNoTracking()
- .Where(a => a.UserId == uid)
- .Select(a => a.DoesCode)
- .Distinct()
- .ToList();
-
- if (allowedActivityCodes.Count == 0)
- {
- return Ok(Array.Empty