diff --git a/.vscode/tasks.json b/.vscode/tasks.json
index 3faedae0..fd46437a 100644
--- a/.vscode/tasks.json
+++ b/.vscode/tasks.json
@@ -7,12 +7,13 @@
"fileLocation": ["relative", "${workspaceFolder}"],
"source": "dotnet",
"pattern": {
- "regexp": "^\\s+(.*)\\((\\d+):(\\d+)\\):\\s+(error|warning)\\s+(.*)$",
+ "regexp": "^\\s+(.*)\\((\\d+),(\\d+)\\):\\s+(error|warning) (.+): (.*)$",
"file": 1,
"line": 2,
"column": 3,
"severity": 4,
- "message": 5
+ "code": 5,
+ "message": 6
}
}
],
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7210a227..b454a3f8 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,21 +1,18 @@
# Changelog
-Toutes les modifications notables de PostIt et de la plateforme Yavsc
-sont documentées dans ce fichier.
+## [1.0.8-rc7] - unstable
-Le format suit [Keep a Changelog](https://keepachangelog.com/fr/1.1.0/),
-et ce projet adhère au [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
+### Added
-À noter : la **parité du numéro de patch** porte une signification de canal :
+* [PostIt] The search pattern now persists
-- **patch pair** (ex. `1.0.0`, `1.0.2`) → **stable**
-- **patch impair** (ex. `1.0.1`, `1.0.3`) → **preview**
-- **suffixe** (ex. `1.0.0-rc1`, `1.0.0-alpha`) → **instable**
+### Changed
-Cette convention est partagée avec le dépôt
-[`postit-debian`](https://forgejo.pschneider.fr/notazof/postit-debian)
-pour la production des paquets `.deb`.
+* The blog spot path is now `/api/v1/blogspot` (yet in last release)
+### Fixed
+
+* [Yavsc.Org][TODO] La forme de l'email de l'utilisateur est maintenant validée avant l'envoi du formulaire d'enregistrement
## [1.0.8-rc6] - unstable
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index b9969ea5..fd408c5c 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -49,6 +49,26 @@ Les tests sont répartis en :
item « Tests d'intégration smoke par BC ».
- `src/PostIt.Tests/` — tests unitaires du client desktop PostIt.
+## Le CHANGELOG.md
+
+Le `CHANGELOG.md` est un document de changement de version
+
+Toutes les modifications notables de PostIt et de la plateforme Yavsc
+sont documentées dans ce fichier.
+
+Le format suit [Keep a Changelog](https://keepachangelog.com/fr/1.1.0/),
+et ce projet adhère au [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
+
+À noter : la **parité du numéro de patch** porte une signification de canal :
+
+- **patch pair** (ex. `1.0.0`, `1.0.2`) → **stable**
+- **patch impair** (ex. `1.0.1`, `1.0.3`) → **preview**
+- **suffixe** (ex. `1.0.0-rc1`, `1.0.0-alpha`) → **instable**
+
+Cette convention est partagée avec le dépôt
+[`postit-debian`](https://forgejo.pschneider.fr/notazof/postit-debian)
+pour la production des paquets `.deb`.
+
## Navigation (PostIt)
La navigation est centralisée dans
diff --git a/Makefile b/Makefile
index fa9d4ecf..08f0461d 100644
--- a/Makefile
+++ b/Makefile
@@ -77,13 +77,6 @@ release:
echo " V : version semver (ex. 1.0.7-rc1) — sert à nommer la branche."; \
exit 1; \
fi
- @CURRENT=$$(git branch --show-current); \
- if [ "$$CURRENT" != "main" ]; then \
- echo "Refus : la cible doit être lancée depuis main."; \
- echo " Branche courante : $$CURRENT"; \
- echo " Fais : git checkout main && git pull --ff-only origin main"; \
- exit 1; \
- fi
@if [ -n "$$(git status --porcelain)" ]; then \
echo "Working tree sale, refus de créer une branche release."; \
git status --short; \
diff --git a/src/PostIt/PostIt.Android/PostIt.Android.csproj b/src/PostIt/PostIt.Android/PostIt.Android.csproj
index 7f44645d..f3b57e87 100644
--- a/src/PostIt/PostIt.Android/PostIt.Android.csproj
+++ b/src/PostIt/PostIt.Android/PostIt.Android.csproj
@@ -11,7 +11,7 @@
false
1.1.0.0
1.1.0.0
- 1.1.0-beta.1+148.Branch.release-1.0.8-rc4.Sha.6b161b0fb509fec0ce467aa94f1a450b202af2f0
+ 1.1.0-beta.1+172.Branch.release-1.0.8-rc6.Sha.c5941e04ced8547b1a25e8c43eb24ddef608b428
1.1.0-beta.1
diff --git a/src/PostIt/PostIt.Browser/PostIt.Browser.csproj b/src/PostIt/PostIt.Browser/PostIt.Browser.csproj
index 67600a72..96857947 100644
--- a/src/PostIt/PostIt.Browser/PostIt.Browser.csproj
+++ b/src/PostIt/PostIt.Browser/PostIt.Browser.csproj
@@ -6,7 +6,7 @@
enable
1.1.0.0
1.1.0.0
- 1.1.0-beta.1+148.Branch.release-1.0.8-rc4.Sha.6b161b0fb509fec0ce467aa94f1a450b202af2f0
+ 1.1.0-beta.1+172.Branch.release-1.0.8-rc6.Sha.c5941e04ced8547b1a25e8c43eb24ddef608b428
1.1.0-beta.1
diff --git a/src/PostIt/PostIt.Desktop/PostIt.Desktop.csproj b/src/PostIt/PostIt.Desktop/PostIt.Desktop.csproj
index 30ba65c4..0f6c5614 100644
--- a/src/PostIt/PostIt.Desktop/PostIt.Desktop.csproj
+++ b/src/PostIt/PostIt.Desktop/PostIt.Desktop.csproj
@@ -7,7 +7,7 @@
enable
1.1.0.0
1.1.0.0
- 1.1.0-beta.1+148.Branch.release-1.0.8-rc4.Sha.6b161b0fb509fec0ce467aa94f1a450b202af2f0
+ 1.1.0-beta.1+172.Branch.release-1.0.8-rc6.Sha.c5941e04ced8547b1a25e8c43eb24ddef608b428
1.1.0-beta.1
diff --git a/src/PostIt/PostIt.Tests/MainPageSaveTests.cs b/src/PostIt/PostIt.Tests/MainPageSaveTests.cs
index 5baae658..cef67f0f 100644
--- a/src/PostIt/PostIt.Tests/MainPageSaveTests.cs
+++ b/src/PostIt/PostIt.Tests/MainPageSaveTests.cs
@@ -81,7 +81,7 @@ public class MainPageSaveTests
Assert.NotEmpty(recorder.Calls);
var (method, path, body) = recorder.FirstCall;
Assert.Equal(HttpMethod.Post, method);
- Assert.Equal("blog", path);
+ Assert.Equal("blogspot", path);
var sent = Assert.IsType(body);
Assert.Equal(typed, sent.Title);
}
diff --git a/src/PostIt/PostIt.Tests/PostIt.Tests.csproj b/src/PostIt/PostIt.Tests/PostIt.Tests.csproj
index 86a7998a..f38bf43f 100644
--- a/src/PostIt/PostIt.Tests/PostIt.Tests.csproj
+++ b/src/PostIt/PostIt.Tests/PostIt.Tests.csproj
@@ -8,7 +8,7 @@
true
1.1.0.0
1.1.0.0
- 1.1.0-beta.1+148.Branch.release-1.0.8-rc4.Sha.6b161b0fb509fec0ce467aa94f1a450b202af2f0
+ 1.1.0-beta.1+172.Branch.release-1.0.8-rc6.Sha.c5941e04ced8547b1a25e8c43eb24ddef608b428
1.1.0-beta.1
diff --git a/src/PostIt/PostIt.Tests/SettingsLoadTests.cs b/src/PostIt/PostIt.Tests/SettingsLoadTests.cs
index 43021caa..3f350240 100644
--- a/src/PostIt/PostIt.Tests/SettingsLoadTests.cs
+++ b/src/PostIt/PostIt.Tests/SettingsLoadTests.cs
@@ -1,3 +1,5 @@
+using System.Text.Json;
+
namespace PostIt.Tests;
public class SettingsLoadTests
@@ -149,4 +151,26 @@ public class SettingsLoadTests
Assert.True(settings.Loaded);
}
+
+ [Fact]
+ public void SearchText_is_serialized_in_settings_and_round_trips()
+ {
+ var settings = new PostIt.ViewModels.Settings
+ {
+ Authentication = new AuthenticationSettings
+ {
+ Authority = "https://example.test/",
+ ClientId = "postit-tests",
+ Scopes = new[] { "openid" }
+ }
+ };
+
+ settings.SearchText = "bonjour";
+
+ var json = JsonSerializer.Serialize(settings);
+ var roundTrip = JsonSerializer.Deserialize(json);
+
+ Assert.NotNull(roundTrip);
+ Assert.Equal("bonjour", roundTrip.SearchText);
+ }
}
diff --git a/src/PostIt/PostIt/App.axaml.cs b/src/PostIt/PostIt/App.axaml.cs
index e3735c1e..f43aa1d9 100644
--- a/src/PostIt/PostIt/App.axaml.cs
+++ b/src/PostIt/PostIt/App.axaml.cs
@@ -157,6 +157,7 @@ private void ConfigureRootView(MainView rootView)
{
var app = (App)Current!;
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 c4e774cc..5f19292a 100644
--- a/src/PostIt/PostIt/Helpers/ServiceCollectionHelpers.cs
+++ b/src/PostIt/PostIt/Helpers/ServiceCollectionHelpers.cs
@@ -42,17 +42,9 @@ public static class ServiceCollectionHelpers
// the navigation stack, each bound to a fresh
// SettingsViewModel and missing any in-flight edits.
services.AddSingleton();
- services.AddTransient();
- services.AddTransient();
- services.AddTransient();
- // Dialogs (modal-light pages): the ViewLocator resolves
- // them when a caller pushes a PostAclDialogViewModel or
- // AddCircleMemberDialogViewModel via App.PushPageAsync.
- // App.PushPageAsync overwrites the page's DataContext with
- // the caller-built VM, so the parameterless ctor is enough
- // here — the parametrised ctors stay for direct test wiring.
- services.AddTransient();
- services.AddTransient();
+ services.AddSingleton();
+ services.AddSingleton();
+ services.AddSingleton();
// ViewModels
services.AddSingleton(settings);
services.AddSingleton(api);
@@ -61,18 +53,25 @@ public static class ServiceCollectionHelpers
services.AddSingleton(blogAclClient);
services.AddSingleton(userSearchClient);
services.AddSingleton(userDirectory);
- services.AddTransient();
- services.AddTransient();
- services.AddTransient();
- services.AddTransient();
+ services.AddSingleton();
+ services.AddSingleton();
+ services.AddSingleton();
+ // Dialogs (modal-light pages): the ViewLocator resolves
+ // them when a caller pushes a PostAclDialogViewModel or
+ // AddCircleMemberDialogViewModel via App.PushPageAsync.
+ // App.PushPageAsync overwrites the page's DataContext with
+ // the caller-built VM, so the parameterless ctor is enough
+ // here — the parametrised ctors stay for direct test wiring.
+ services.AddTransient();
+ services.AddTransient();
// Persistent session banner: one instance for the lifetime of
// the app so the same VM survives page navigation.
var sessionStatus = new SessionStatusViewModel { Api = api };
sessionStatus.Refresh();
services.AddSingleton(sessionStatus);
- services.AddTransient();
-
+ services.AddSingleton();
+ services.AddSingleton();
return services.BuildServiceProvider();
}
}
diff --git a/src/PostIt/PostIt/PostIt.csproj b/src/PostIt/PostIt/PostIt.csproj
index 1ae36a44..e5ee0d40 100644
--- a/src/PostIt/PostIt/PostIt.csproj
+++ b/src/PostIt/PostIt/PostIt.csproj
@@ -5,7 +5,7 @@
latest
1.1.0.0
1.1.0.0
- 1.1.0-beta.1+148.Branch.release-1.0.8-rc4.Sha.6b161b0fb509fec0ce467aa94f1a450b202af2f0
+ 1.1.0-beta.1+172.Branch.release-1.0.8-rc6.Sha.c5941e04ced8547b1a25e8c43eb24ddef608b428
1.1.0-beta.1
diff --git a/src/PostIt/PostIt/ViewModels/MainViewModel.cs b/src/PostIt/PostIt/ViewModels/MainViewModel.cs
index 1aa3eee0..2b065cae 100644
--- a/src/PostIt/PostIt/ViewModels/MainViewModel.cs
+++ b/src/PostIt/PostIt/ViewModels/MainViewModel.cs
@@ -48,7 +48,7 @@ public partial class MainViewModel : ViewModelBase
/// mutable field. Toggling is its own action.
[ObservableProperty]
public partial bool DraftIsPublished { get; set; }
-
+ public bool IsLoaded { get; private set; }
public Settings SettingsModel { get; }
[ObservableProperty]
@@ -72,6 +72,194 @@ public partial class MainViewModel : ViewModelBase
[ObservableProperty]
public partial Settings Settings { get; private set; }
+ [RelayCommand]
+ internal async Task RefreshAsync()
+ {
+ await ExecuteAsync(async () =>
+ {
+ var posts = await BlogClient!.GetPostsAsync();
+ Posts.Clear();
+ foreach (var post in posts.OrderByDescending(p => p.DateModified))
+ {
+ Posts.Add(post);
+ }
+ ApplyFilter();
+ StatusMessage = $"Loaded {Posts.Count} posts.";
+ });
+ }
+
+ [RelayCommand]
+ internal async Task SearchAsync() {
+ await RefreshAsync();
+ ApplyFilter();
+ }
+
+ [RelayCommand]
+ internal async Task SaveAsync()
+ {
+ // The button is already disabled when the title is empty
+ // (see CanSave), but the test path (and any programmatic
+ // ICommand.Execute) bypasses CanExecute, so we still
+ // guard here. Better to no-op with a status message
+ // than to send a request the server will reject.
+ if (string.IsNullOrWhiteSpace(DraftTitle))
+ {
+ StatusMessage = "Title is required.";
+ return;
+ }
+
+ await ExecuteAsync(async () =>
+ {
+ // Build a fresh BlogPostDto from the editor buffer on
+ // every Save — we no longer mutate SelectedPost in
+ // 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
+ // input; the new shape keeps the editor buffer as
+ // the single source of truth for outgoing payloads
+ // and the selected post as a read-only hint for
+ // the update path.
+ if (SelectedPost is null || SelectedPost.Id == 0)
+ {
+ var draft = new BlogPostDto
+ {
+ Title = DraftTitle,
+ Article = DraftArticle ?? string.Empty,
+ DateCreated = DateTime.UtcNow,
+ DateModified = DateTime.UtcNow,
+ IsPublished = DraftIsPublished
+ };
+ var created = await BlogClient!.CreatePostAsync(draft);
+ if (created is not null)
+ {
+ SelectedPost = created;
+ StatusMessage = $"Created post {created.Id}.";
+ }
+ }
+ else
+ {
+ var update = new BlogPostDto
+ {
+ Id = SelectedPost.Id,
+ AuthorId = SelectedPost.AuthorId,
+ Photo = SelectedPost.Photo,
+ Title = DraftTitle,
+ Article = DraftArticle ?? string.Empty,
+ DateCreated = SelectedPost.DateCreated,
+ DateModified = DateTime.UtcNow,
+ };
+ await BlogClient!.UpdatePostAsync(SelectedPost.Id, update);
+ StatusMessage = $"Saved post {SelectedPost.Id}.";
+ }
+
+ await RefreshPostsAsync();
+ });
+ }
+
+ [RelayCommand]
+ internal async Task DeleteAsync()
+ {
+ if (SelectedPost is null || SelectedPost.Id == 0)
+ {
+ StatusMessage = "Select an existing post before deleting.";
+ return;
+ }
+
+ await ExecuteAsync(async () =>
+ {
+ await BlogClient!.DeletePostAsync(SelectedPost.Id);
+ StatusMessage = $"Deleted post {SelectedPost.Id}.";
+ SelectedPost = null;
+ await RefreshPostsAsync();
+ });
+ }
+
+ ///
+ /// Toggle the publication state of the currently selected
+ /// post. Pushes the new state to
+ /// PUT /api/BlogApi/{id}/publish and reflects it
+ /// locally in + the
+ /// selected post so the UI updates without a full
+ /// refresh.
+ ///
+ /// The toggle is its own action — separate from Save
+ /// — because Publish is not part of the
+ /// BlogPostDto payload. Bundling it into Save
+ /// would require a wire-shape change and a second server
+ /// overload; the dedicated endpoint keeps the wire
+ /// contract clean.
+ ///
+ [RelayCommand]
+ internal async Task TogglePublishAsync()
+ {
+ if (SelectedPost is null || SelectedPost.Id == 0)
+ {
+ StatusMessage = "Sélectionnez un billet existant pour changer sa publication.";
+ return;
+ }
+
+ await ExecuteAsync(async () =>
+ {
+ var desired = !DraftIsPublished;
+ await BlogClient!.SetPublishAsync(SelectedPost.Id, desired);
+ DraftIsPublished = desired;
+ // Mirror into the selected post so a subsequent
+ // RefreshPostsAsync() doesn't blow away the
+ // locally flipped state until the round-trip
+ // re-hydrates it.
+ SelectedPost.IsPublished = desired;
+ StatusMessage = desired
+ ? $"Billet {SelectedPost.Id} publié."
+ : $"Billet {SelectedPost.Id} remis en brouillon.";
+ });
+ }
+
+ ///
+ /// DEV ONLY: open the signature capture page. The production
+ /// entry point is a SignalR push from Yavsc.Org ("devis
+ /// received, sign here"); this command is the dev-time
+ /// shortcut to reach the page without that infrastructure.
+ /// Aligned on the same VM-first navigation pattern as
+ /// : the VM resolves the target VM
+ /// through , the ViewLocator picks
+ /// the matching Control at bind time. No
+ /// Click handler, no App.ServiceProvider
+ /// access from the view layer.
+ ///
+ [RelayCommand]
+ internal async Task OpenSignatureDevAsync()
+ {
+ await ((App)App.Current!).PushPageAsync(SignatureModel).ConfigureAwait(true);
+ }
+
+
+ [RelayCommand(CanExecute = nameof(CanManageAcl))]
+ public async Task ManageAclAsync()
+ {
+ if (SelectedPost is null)
+ {
+ StatusMessage = "Select an existing post before managing ACL.";
+ return;
+ }
+ await ((App)App.Current!).PushPageAsync(GetACLViewModel(SelectedPost)).ConfigureAwait(true);
+ }
+
+ [RelayCommand]
+ public async Task OpenCirclesAsync()
+ {
+ var circlesVm = ResolveServices().GetRequiredService();
+ await ((App)App.Current!).PushPageAsync(circlesVm).ConfigureAwait(true);
+ }
+
+ private ViewModelBase GetACLViewModel(BlogPostDto selectedPost)
+ {
+ var sp = ResolveServices();
+ var aclClient = sp.GetRequiredService();
+ var circleClient = sp.GetRequiredService();
+ return new PostAclDialogViewModel(selectedPost, aclClient, circleClient);
+ }
+
///
/// API surface that hits the Yavsc.Blogs deployment at
/// . Owned and constructed by
@@ -130,17 +318,18 @@ public partial class MainViewModel : ViewModelBase
private void Init(Settings? settings)
{
- SearchText = string.Empty;
Posts = new ObservableCollection();
FilteredPosts = new ObservableCollection();
SelectedPost = null;
IsBusy = false;
StatusMessage = "Ready";
Settings = settings ?? new Settings();
+ SearchText = Settings.SearchText;
WindowTitle = "PostIt";
DraftTitle = string.Empty;
DraftArticle = string.Empty;
DraftIsPublished = false;
+ IsLoaded = false;
// Production path: DI injects the canonical Settings singleton
// and we use it as-is. Test path: tests call this constructor
// without a Settings argument; we fall back to a fresh
@@ -152,6 +341,15 @@ public partial class MainViewModel : ViewModelBase
// (thread-safe dispatcher marshalling) so the duplicate
// instance is now merely wasteful, not dangerous.
+ Settings.PropertyChanged += (s, e) =>
+ {
+ if (e.PropertyName == nameof(Settings.SearchText))
+ {
+ SearchText = Settings.SearchText;
+ ApplyFilter();
+ }
+ };
+
}
/// Save is enabled as soon as the user has typed
@@ -179,7 +377,14 @@ public partial class MainViewModel : ViewModelBase
Init(settings);
}
- partial void OnSearchTextChanged(string value) => ApplyFilter();
+ partial void OnSearchTextChanged(string value)
+ {
+ if (Settings is not null && Settings.SearchText != value)
+ {
+ Settings.SearchText = value;
+ }
+ ApplyFilter();
+ }
partial void OnSelectedPostChanged(BlogPostDto? value)
{
@@ -206,170 +411,6 @@ public partial class MainViewModel : ViewModelBase
partial void OnDraftTitleChanged(string value) => SaveCommand.NotifyCanExecuteChanged();
partial void OnDraftArticleChanged(string value) => SaveCommand.NotifyCanExecuteChanged();
- [RelayCommand]
- internal async Task LoadPosts()
- {
- await ExecuteAsync(async () =>
- {
- var posts = await BlogClient!.GetPostsAsync();
- Posts.Clear();
- foreach (var post in posts.OrderByDescending(p => p.DateModified))
- {
- Posts.Add(post);
- }
- ApplyFilter();
- StatusMessage = $"Loaded {Posts.Count} posts.";
- });
- }
-
- [RelayCommand]
- internal void Search() => ApplyFilter();
-
- [RelayCommand]
- internal async Task Save()
- {
- // The button is already disabled when the title is empty
- // (see CanSave), but the test path (and any programmatic
- // ICommand.Execute) bypasses CanExecute, so we still
- // guard here. Better to no-op with a status message
- // than to send a request the server will reject.
- if (string.IsNullOrWhiteSpace(DraftTitle))
- {
- StatusMessage = "Title is required.";
- return;
- }
-
- await ExecuteAsync(async () =>
- {
- // Build a fresh BlogPostDto from the editor buffer on
- // every Save — we no longer mutate SelectedPost in
- // 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
- // input; the new shape keeps the editor buffer as
- // the single source of truth for outgoing payloads
- // and the selected post as a read-only hint for
- // the update path.
- if (SelectedPost is null || SelectedPost.Id == 0)
- {
- var draft = new BlogPostDto
- {
- Title = DraftTitle,
- Article = DraftArticle ?? string.Empty,
- DateCreated = DateTime.UtcNow,
- DateModified = DateTime.UtcNow,
- };
- var created = await BlogClient!.CreatePostAsync(draft);
- if (created is not null)
- {
- SelectedPost = created;
- StatusMessage = $"Created post {created.Id}.";
- }
- }
- else
- {
- var update = new BlogPostDto
- {
- Id = SelectedPost.Id,
- AuthorId = SelectedPost.AuthorId,
- Photo = SelectedPost.Photo,
- Title = DraftTitle,
- Article = DraftArticle ?? string.Empty,
- DateCreated = SelectedPost.DateCreated,
- DateModified = DateTime.UtcNow,
- };
- await BlogClient!.UpdatePostAsync(SelectedPost.Id, update);
- StatusMessage = $"Saved post {SelectedPost.Id}.";
- }
-
- await RefreshPostsAsync();
- });
- }
-
- [RelayCommand]
- internal async Task Delete()
- {
- if (SelectedPost is null || SelectedPost.Id == 0)
- {
- StatusMessage = "Select an existing post before deleting.";
- return;
- }
-
- await ExecuteAsync(async () =>
- {
- await BlogClient!.DeletePostAsync(SelectedPost.Id);
- StatusMessage = $"Deleted post {SelectedPost.Id}.";
- SelectedPost = null;
- await RefreshPostsAsync();
- });
- }
-
- ///
- /// Toggle the publication state of the currently selected
- /// post. Pushes the new state to
- /// PUT /api/BlogApi/{id}/publish and reflects it
- /// locally in + the
- /// selected post so the UI updates without a full
- /// refresh.
- ///
- /// The toggle is its own action — separate from Save
- /// — because Publish is not part of the
- /// BlogPostDto payload. Bundling it into Save
- /// would require a wire-shape change and a second server
- /// overload; the dedicated endpoint keeps the wire
- /// contract clean.
- ///
- [RelayCommand]
- internal async Task TogglePublish()
- {
- if (SelectedPost is null || SelectedPost.Id == 0)
- {
- StatusMessage = "Sélectionnez un billet existant pour changer sa publication.";
- return;
- }
-
- await ExecuteAsync(async () =>
- {
- var desired = !DraftIsPublished;
- await BlogClient!.SetPublishAsync(SelectedPost.Id, desired);
- DraftIsPublished = desired;
- // Mirror into the selected post so a subsequent
- // RefreshPostsAsync() doesn't blow away the
- // locally flipped state until the round-trip
- // re-hydrates it.
- SelectedPost.IsPublished = desired;
- StatusMessage = desired
- ? $"Billet {SelectedPost.Id} publié."
- : $"Billet {SelectedPost.Id} remis en brouillon.";
- });
- }
-
- ///
- /// DEV ONLY: open the signature capture page. The production
- /// entry point is a SignalR push from Yavsc.Org ("devis
- /// received, sign here"); this command is the dev-time
- /// shortcut to reach the page without that infrastructure.
- /// Aligned on the same VM-first navigation pattern as
- /// : the VM resolves the target VM
- /// through , the ViewLocator picks
- /// the matching Control at bind time. No
- /// Click handler, no App.ServiceProvider
- /// access from the view layer.
- ///
- [RelayCommand]
- internal async Task OpenSignatureDev()
- {
- await ((App)App.Current!).PushPageAsync(SignatureModel).ConfigureAwait(true);
- }
-
- private ViewModelBase GetACLViewModel(BlogPostDto selectedPost)
- {
- var sp = ResolveServices();
- var aclClient = sp.GetRequiredService();
- var circleClient = sp.GetRequiredService();
- return new PostAclDialogViewModel(selectedPost, aclClient, circleClient);
- }
private async Task RefreshPostsAsync()
{
@@ -426,28 +467,18 @@ public partial class MainViewModel : ViewModelBase
private void UpdateCommandStates()
{
- LoadPostsCommand.NotifyCanExecuteChanged();
+ RefreshCommand.NotifyCanExecuteChanged();
SaveCommand.NotifyCanExecuteChanged();
DeleteCommand.NotifyCanExecuteChanged();
}
-
- [RelayCommand(CanExecute = nameof(CanManageAcl))]
- public async Task ManageAcl()
+ internal async Task InitializeAsync()
{
- if (SelectedPost is null)
+ if (!IsLoaded)
{
- StatusMessage = "Select an existing post before managing ACL.";
- return;
+ await RefreshAsync();
+ IsLoaded = true;
}
- await ((App)App.Current!).PushPageAsync(GetACLViewModel(SelectedPost)).ConfigureAwait(true);
- }
-
- [RelayCommand]
- public async Task OpenCircles()
- {
- var circlesVm = ResolveServices().GetRequiredService();
- await ((App)App.Current!).PushPageAsync(circlesVm).ConfigureAwait(true);
}
}
diff --git a/src/PostIt/PostIt/ViewModels/Settings.cs b/src/PostIt/PostIt/ViewModels/Settings.cs
index fd87d772..8586fa34 100644
--- a/src/PostIt/PostIt/ViewModels/Settings.cs
+++ b/src/PostIt/PostIt/ViewModels/Settings.cs
@@ -28,6 +28,9 @@ public partial class Settings : ViewModelBase
[ObservableProperty]
public partial string BusinessApiUrl { get; set; } = "https://business.pschneider.fr/api/v1/";
+ [ObservableProperty]
+ public partial string SearchText { get; set; } = string.Empty;
+
///
/// Catch top-level mutations: the four ObservableProperty
/// setters above all funnel through here, and we flip
@@ -43,6 +46,7 @@ public partial class Settings : ViewModelBase
partial void OnDarkModeChanged(bool value) => MarkDirty();
partial void OnBlogsApiUrlChanged(string value) => MarkDirty();
partial void OnBusinessApiUrlChanged(string value) => MarkDirty();
+ partial void OnSearchTextChanged(string value) => MarkDirty();
///
/// Authentication can be reassigned wholesale by
@@ -295,6 +299,7 @@ public partial class Settings : ViewModelBase
{
this.Authentication = settings.Authentication;
this.DarkMode = settings.DarkMode;
+ this.SearchText = settings.SearchText ?? string.Empty;
if (!(settings.Authentication is null))
{
this.Authentication = new AuthenticationSettings();
@@ -348,6 +353,7 @@ public partial class Settings : ViewModelBase
Scopes = AuthenticationSettings.DefaultScopes
};
this.DarkMode = false;
+ this.SearchText = string.Empty;
}
///
diff --git a/src/PostIt/PostIt/Views/MainPage.axaml b/src/PostIt/PostIt/Views/MainPage.axaml
index e44f9f99..0d2f5411 100644
--- a/src/PostIt/PostIt/Views/MainPage.axaml
+++ b/src/PostIt/PostIt/Views/MainPage.axaml
@@ -29,15 +29,15 @@
VerticalAlignment="Top">
-
-
-
-
+
+
+
+
diff --git a/src/PostIt/PostIt/Views/MainPage.axaml.cs b/src/PostIt/PostIt/Views/MainPage.axaml.cs
index 17d3a825..97e3f922 100644
--- a/src/PostIt/PostIt/Views/MainPage.axaml.cs
+++ b/src/PostIt/PostIt/Views/MainPage.axaml.cs
@@ -1,4 +1,6 @@
+using System;
using Avalonia.Controls;
+using Avalonia.Controls.Primitives;
namespace PostIt.Views;
@@ -8,4 +10,16 @@ public partial class MainPage : ContentPage
{
InitializeComponent();
}
-}
\ No newline at end of file
+
+ 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/MainView.axaml.cs b/src/PostIt/PostIt/Views/MainView.axaml.cs
index 74f46686..654b90e2 100644
--- a/src/PostIt/PostIt/Views/MainView.axaml.cs
+++ b/src/PostIt/PostIt/Views/MainView.axaml.cs
@@ -1,3 +1,4 @@
+using System;
using Avalonia.Controls;
namespace PostIt.Views;
@@ -8,4 +9,16 @@ public partial class MainView : UserControl
{
InitializeComponent();
}
+
+ protected override void OnDataContextChanged(EventArgs e)
+ {
+ base.OnDataContextChanged(e);
+ if (DataContext is ViewModels.MainViewModel vm)
+ {
+ if (!vm.IsLoaded)
+ {
+ vm.RefreshAsync().Wait();
+ }
+ }
+ }
}
diff --git a/src/Yavsc.Abstract/Yavsc.Abstract.csproj b/src/Yavsc.Abstract/Yavsc.Abstract.csproj
index 4a9c75bc..554cb196 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-beta.1+148.Branch.release-1.0.8-rc4.Sha.6b161b0fb509fec0ce467aa94f1a450b202af2f0
+ 1.1.0-beta.1+172.Branch.release-1.0.8-rc6.Sha.c5941e04ced8547b1a25e8c43eb24ddef608b428
1.1.0-beta.1
\ No newline at end of file
diff --git a/src/Yavsc.Api.Client/Yavsc.Api.Client.csproj b/src/Yavsc.Api.Client/Yavsc.Api.Client.csproj
index 44e4eb27..db7aa466 100644
--- a/src/Yavsc.Api.Client/Yavsc.Api.Client.csproj
+++ b/src/Yavsc.Api.Client/Yavsc.Api.Client.csproj
@@ -17,7 +17,7 @@
true
1.1.0.0
1.1.0.0
- 1.1.0-beta.1+148.Branch.release-1.0.8-rc4.Sha.6b161b0fb509fec0ce467aa94f1a450b202af2f0
+ 1.1.0-beta.1+172.Branch.release-1.0.8-rc6.Sha.c5941e04ced8547b1a25e8c43eb24ddef608b428
1.1.0-beta.1
diff --git a/src/Yavsc.Api/Yavsc.Api.csproj b/src/Yavsc.Api/Yavsc.Api.csproj
index 20bc2eb6..0f8054e4 100644
--- a/src/Yavsc.Api/Yavsc.Api.csproj
+++ b/src/Yavsc.Api/Yavsc.Api.csproj
@@ -7,7 +7,7 @@
true
1.1.0.0
1.1.0.0
- 1.1.0-beta.1+148.Branch.release-1.0.8-rc4.Sha.6b161b0fb509fec0ce467aa94f1a450b202af2f0
+ 1.1.0-beta.1+172.Branch.release-1.0.8-rc6.Sha.c5941e04ced8547b1a25e8c43eb24ddef608b428
1.1.0-beta.1
diff --git a/src/Yavsc.Blogs.Tests/Yavsc.Blogs.Tests.csproj b/src/Yavsc.Blogs.Tests/Yavsc.Blogs.Tests.csproj
index 7763cf07..96616e99 100644
--- a/src/Yavsc.Blogs.Tests/Yavsc.Blogs.Tests.csproj
+++ b/src/Yavsc.Blogs.Tests/Yavsc.Blogs.Tests.csproj
@@ -9,7 +9,7 @@
true
1.1.0.0
1.1.0.0
- 1.1.0-beta.1+148.Branch.release-1.0.8-rc4.Sha.6b161b0fb509fec0ce467aa94f1a450b202af2f0
+ 1.1.0-beta.1+172.Branch.release-1.0.8-rc6.Sha.c5941e04ced8547b1a25e8c43eb24ddef608b428
1.1.0-beta.1
diff --git a/src/Yavsc.Blogs/Yavsc.Blogs.csproj b/src/Yavsc.Blogs/Yavsc.Blogs.csproj
index 834bc57f..7f262340 100644
--- a/src/Yavsc.Blogs/Yavsc.Blogs.csproj
+++ b/src/Yavsc.Blogs/Yavsc.Blogs.csproj
@@ -8,7 +8,7 @@
true
1.1.0.0
1.1.0.0
- 1.1.0-beta.1+148.Branch.release-1.0.8-rc4.Sha.6b161b0fb509fec0ce467aa94f1a450b202af2f0
+ 1.1.0-beta.1+172.Branch.release-1.0.8-rc6.Sha.c5941e04ced8547b1a25e8c43eb24ddef608b428
1.1.0-beta.1
diff --git a/src/Yavsc.Org.Tests/Directory.Packages.props b/src/Yavsc.Org.Tests/Directory.Packages.props
index b267eafe..5927d2b5 100644
--- a/src/Yavsc.Org.Tests/Directory.Packages.props
+++ b/src/Yavsc.Org.Tests/Directory.Packages.props
@@ -7,7 +7,5 @@
-
-
diff --git a/src/Yavsc.Org.Tests/NonRegression/EMailling.cs b/src/Yavsc.Org.Tests/NonRegression/EMailling.cs
index 743dbbc1..5b1b33e9 100644
--- a/src/Yavsc.Org.Tests/NonRegression/EMailling.cs
+++ b/src/Yavsc.Org.Tests/NonRegression/EMailling.cs
@@ -55,5 +55,6 @@ namespace Yavsc.Org.Tests
client.Calls.Select(c => c.Kind).ToArray());
Assert.Equal(_serverFixture.SiteSettings.Owner.EMail, client.LastSentMessage?.To.Mailboxes.First().Address);
}
+
}
}
diff --git a/src/Yavsc.Org.Tests/Yavsc.Org.Tests.csproj b/src/Yavsc.Org.Tests/Yavsc.Org.Tests.csproj
index 747498d4..8be07946 100644
--- a/src/Yavsc.Org.Tests/Yavsc.Org.Tests.csproj
+++ b/src/Yavsc.Org.Tests/Yavsc.Org.Tests.csproj
@@ -11,7 +11,7 @@
$(MSBuildProjectDirectory)\test.runsettings
1.1.0.0
1.1.0.0
- 1.1.0-beta.1+148.Branch.release-1.0.8-rc4.Sha.6b161b0fb509fec0ce467aa94f1a450b202af2f0
+ 1.1.0-beta.1+172.Branch.release-1.0.8-rc6.Sha.c5941e04ced8547b1a25e8c43eb24ddef608b428
1.1.0-beta.1
diff --git a/src/Yavsc.Org/Directory.Packages.props b/src/Yavsc.Org/Directory.Packages.props
index fd16374b..d9925e02 100644
--- a/src/Yavsc.Org/Directory.Packages.props
+++ b/src/Yavsc.Org/Directory.Packages.props
@@ -20,6 +20,5 @@
-
diff --git a/src/Yavsc.Org/Yavsc.Org.csproj b/src/Yavsc.Org/Yavsc.Org.csproj
index a382dd09..d4ae0787 100644
--- a/src/Yavsc.Org/Yavsc.Org.csproj
+++ b/src/Yavsc.Org/Yavsc.Org.csproj
@@ -9,7 +9,7 @@
https://github.com/pazof/yavsc
1.1.0.0
1.1.0.0
- 1.1.0-beta.1+148.Branch.release-1.0.8-rc4.Sha.6b161b0fb509fec0ce467aa94f1a450b202af2f0
+ 1.1.0-beta.1+172.Branch.release-1.0.8-rc6.Sha.c5941e04ced8547b1a25e8c43eb24ddef608b428
1.1.0-beta.1
diff --git a/src/Yavsc.Server/Services/MailSender.cs b/src/Yavsc.Server/Services/MailSender.cs
index 417fca6b..d8b3c46c 100644
--- a/src/Yavsc.Server/Services/MailSender.cs
+++ b/src/Yavsc.Server/Services/MailSender.cs
@@ -9,6 +9,7 @@ using Yavsc.Settings;
using Yavsc.Models;
using Microsoft.AspNetCore.Identity.UI.Services;
using Microsoft.Extensions.Localization;
+using System.Text.RegularExpressions;
using System.Web;
namespace Yavsc.Services
@@ -53,45 +54,96 @@ namespace Yavsc.Services
///
public Task SendEmailAsync(string email, string subject, string htmlMessage)
{
- return SendEmailAsync("", email, subject, htmlMessage);
+ return SendEmailAsync(null, email, subject, htmlMessage);
+ }
+
+ internal static MailboxAddress BuildMailboxAddress(string? displayName, string? rawAddress)
+ {
+ if (string.IsNullOrWhiteSpace(rawAddress))
+ {
+ throw new FormatException("Email address is empty.");
+ }
+
+ var candidate = rawAddress.Trim();
+ if (candidate.Contains('<') || candidate.Contains('>'))
+ {
+ var emailMatch = Regex.Match(candidate,
+ @"[A-Za-z0-9.!#$%&'*+/=?^_`{|}~-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}",
+ RegexOptions.CultureInvariant | RegexOptions.IgnoreCase);
+
+ if (emailMatch.Success)
+ {
+ candidate = emailMatch.Value;
+ }
+ else
+ {
+ candidate = candidate.Trim('<', '>', '"', '\'');
+ }
+ }
+
+ candidate = candidate.Trim('"', '\'', '<', '>', ' ');
+ candidate = candidate.Replace(" ", string.Empty);
+
+ if (!MailboxAddress.TryParse(candidate, out var parsedAddress))
+ {
+ throw new FormatException($"Invalid email address '{rawAddress}'.");
+ }
+
+ var safeName = string.IsNullOrWhiteSpace(displayName)
+ ? parsedAddress.Name
+ : displayName.Trim();
+
+ return new MailboxAddress(safeName ?? string.Empty, parsedAddress.Address);
}
public async Task SendEmailAsync(string name, string email, string subject, string htmlMessage)
{
- logger.LogInformation($"SendEmail for {email} : {subject}");
- MimeMessage msg = new();
- msg.From.Add(new MailboxAddress(siteSettings.Owner.Name,
- siteSettings.Owner.EMail));
- msg.To.Add(new MailboxAddress(name, email));
- TextPart text;
- msg.Body = text = new TextPart("html")
+ try
{
- Text = $"{htmlMessage}"
- };
-
- msg.Subject = subject;
- msg.MessageId = MimeKit.Utils.MimeUtils.GenerateMessageId(
- siteSettings.Authority
- );
- using ISmtpClient sc = _smtpClientFactory.CreateClient();
- {
- sc.Timeout = 30000;
- sc.Connect(
- smtpSettings.Host,
- smtpSettings.Port,
- SecureSocketOptions.Auto
- );
-
- if (smtpSettings.UserName != null)
+ logger.LogInformation($"SendEmail for {email} : {subject}");
+ MimeMessage msg = new();
+ msg.From.Add(BuildMailboxAddress(siteSettings.Owner.Name, siteSettings.Owner.EMail));
+ msg.To.Add(BuildMailboxAddress(name, email));
+ TextPart text;
+ msg.Body = text = new TextPart("html")
{
- sc.Authenticate(smtpSettings.UserName, smtpSettings.Password);
- }
+ Text = $"{htmlMessage}"
+ };
- await sc.SendAsync(msg);
- logger.LogInformation($"Sent : {msg.MessageId}");
- sc.Disconnect(true);
+ msg.Subject = subject;
+ msg.MessageId = MimeKit.Utils.MimeUtils.GenerateMessageId(
+ siteSettings.Authority
+ );
+ using ISmtpClient sc = _smtpClientFactory.CreateClient();
+ {
+ sc.Timeout = 30000;
+ sc.Connect(
+ smtpSettings.Host,
+ smtpSettings.Port,
+ SecureSocketOptions.Auto
+ );
+
+ if (smtpSettings.UserName != null)
+ {
+ sc.Authenticate(smtpSettings.UserName, smtpSettings.Password);
+ }
+
+ await sc.SendAsync(msg);
+ logger.LogInformation($"Sent : {msg.MessageId}");
+ sc.Disconnect(true);
+ }
+ return msg.MessageId;
+ }
+ catch (FormatException ex)
+ {
+ logger.LogError(ex, "Refusing to send email because the recipient or sender address is malformed. To={To}, From={From}", email, siteSettings.Owner.EMail);
+ return string.Empty;
+ }
+ catch (Exception ex)
+ {
+ logger.LogError(ex, "Failed to send email. To={To}, Subject={Subject}", email, subject);
+ throw;
}
- return msg.MessageId;
}
public void SendEmailFromCriteria(string Criteria)
diff --git a/src/Yavsc.Server/Yavsc.Server.csproj b/src/Yavsc.Server/Yavsc.Server.csproj
index 2d7ba0fa..c35a24d4 100644
--- a/src/Yavsc.Server/Yavsc.Server.csproj
+++ b/src/Yavsc.Server/Yavsc.Server.csproj
@@ -9,7 +9,7 @@
true
1.1.0.0
1.1.0.0
- 1.1.0-beta.1+148.Branch.release-1.0.8-rc4.Sha.6b161b0fb509fec0ce467aa94f1a450b202af2f0
+ 1.1.0-beta.1+172.Branch.release-1.0.8-rc6.Sha.c5941e04ced8547b1a25e8c43eb24ddef608b428
1.1.0-beta.1
diff --git a/src/Yavsc.Tests.Shared/Yavsc.Tests.Shared.csproj b/src/Yavsc.Tests.Shared/Yavsc.Tests.Shared.csproj
index 6e7a4c5a..89698853 100644
--- a/src/Yavsc.Tests.Shared/Yavsc.Tests.Shared.csproj
+++ b/src/Yavsc.Tests.Shared/Yavsc.Tests.Shared.csproj
@@ -14,7 +14,7 @@
-->
1.1.0.0
1.1.0.0
- 1.1.0-beta.1+148.Branch.release-1.0.8-rc4.Sha.6b161b0fb509fec0ce467aa94f1a450b202af2f0
+ 1.1.0-beta.1+172.Branch.release-1.0.8-rc6.Sha.c5941e04ced8547b1a25e8c43eb24ddef608b428
1.1.0-beta.1
@@ -26,4 +26,4 @@
-
+
\ No newline at end of file
diff --git a/src/cli/cli.csproj b/src/cli/cli.csproj
index 209440d8..0bb14811 100644
--- a/src/cli/cli.csproj
+++ b/src/cli/cli.csproj
@@ -7,7 +7,7 @@
true
1.1.0.0
1.1.0.0
- 1.1.0-beta.1+148.Branch.release-1.0.8-rc4.Sha.6b161b0fb509fec0ce467aa94f1a450b202af2f0
+ 1.1.0-beta.1+172.Branch.release-1.0.8-rc6.Sha.c5941e04ced8547b1a25e8c43eb24ddef608b428
1.1.0-beta.1