Just post one, at least
This commit is contained in:
parent
03cd9843d3
commit
abfc68a809
2 changed files with 91 additions and 33 deletions
|
|
@ -11,8 +11,28 @@ namespace PostIt.ViewModels;
|
||||||
|
|
||||||
public partial class MainPageViewModel : ViewModelBase
|
public partial class MainPageViewModel : ViewModelBase
|
||||||
{
|
{
|
||||||
|
/// <summary>Window/tab title. Cosmetic — bound by
|
||||||
|
/// <c>MainPage.axaml</c> if at all. Not the post title.</summary>
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
public partial string Title { get; set; }
|
public partial string WindowTitle { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Editor buffer for the post title. Bound TwoWay to
|
||||||
|
/// the title <c>TextBox</c> in <c>MainPage.axaml</c>. The Save
|
||||||
|
/// command reads from this buffer (not from
|
||||||
|
/// <see cref="SelectedPost"/>) so that typing into a freshly
|
||||||
|
/// mounted editor (no post selected yet) is captured. With the
|
||||||
|
/// previous "{Binding SelectedPost.Title}" binding, the user's
|
||||||
|
/// keystrokes were silently dropped whenever
|
||||||
|
/// <c>SelectedPost was null</c>, which made the editor a trap
|
||||||
|
/// and caused Save to POST a <c>BlogPost</c> with an empty
|
||||||
|
/// title — hence the 400 "The Title field is required".</summary>
|
||||||
|
[ObservableProperty]
|
||||||
|
public partial string DraftTitle { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Editor buffer for the post body. Same pattern as
|
||||||
|
/// <see cref="DraftTitle"/>.</summary>
|
||||||
|
[ObservableProperty]
|
||||||
|
public partial string DraftArticle { get; set; }
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
public partial ViewModelBase? CurrentViewModel { get; set; }
|
public partial ViewModelBase? CurrentViewModel { get; set; }
|
||||||
|
|
@ -78,7 +98,9 @@ public partial class MainPageViewModel : ViewModelBase
|
||||||
// (thread-safe dispatcher marshalling) so the duplicate
|
// (thread-safe dispatcher marshalling) so the duplicate
|
||||||
// instance is now merely wasteful, not dangerous.
|
// instance is now merely wasteful, not dangerous.
|
||||||
Settings = settings ?? new Settings();
|
Settings = settings ?? new Settings();
|
||||||
Title = "PostIt";
|
WindowTitle = "PostIt";
|
||||||
|
DraftTitle = string.Empty;
|
||||||
|
DraftArticle = string.Empty;
|
||||||
CurrentViewModel = this;
|
CurrentViewModel = this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -97,10 +119,28 @@ public partial class MainPageViewModel : ViewModelBase
|
||||||
|
|
||||||
partial void OnSearchTextChanged(string value) => ApplyFilter();
|
partial void OnSearchTextChanged(string value) => ApplyFilter();
|
||||||
|
|
||||||
partial void OnSelectedPostChanged(BlogPost? value) => UpdateCommandStates();
|
partial void OnSelectedPostChanged(BlogPost? value)
|
||||||
|
{
|
||||||
|
// Mirror the selection into the editor buffer so the
|
||||||
|
// XAML-bound TextBox/TextEditor show the right content
|
||||||
|
// when the user clicks a post in the list. When the
|
||||||
|
// selection is cleared (e.g. after a successful create
|
||||||
|
// rebinds to the server-issued record, or Delete
|
||||||
|
// nulls it out), the buffer is reset so the editor
|
||||||
|
// doesn't show stale content.
|
||||||
|
DraftTitle = value?.Title ?? string.Empty;
|
||||||
|
DraftArticle = value?.Article ?? string.Empty;
|
||||||
|
UpdateCommandStates();
|
||||||
|
}
|
||||||
|
|
||||||
partial void OnIsBusyChanged(bool value) => UpdateCommandStates();
|
partial void OnIsBusyChanged(bool value) => UpdateCommandStates();
|
||||||
|
|
||||||
|
// Save's CanExecute depends on the buffer: the button must
|
||||||
|
// enable as soon as the user has typed a non-whitespace
|
||||||
|
// title, regardless of whether a post is selected.
|
||||||
|
partial void OnDraftTitleChanged(string value) => SaveCommand.NotifyCanExecuteChanged();
|
||||||
|
partial void OnDraftArticleChanged(string value) => SaveCommand.NotifyCanExecuteChanged();
|
||||||
|
|
||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
internal async Task LoadPosts()
|
internal async Task LoadPosts()
|
||||||
{
|
{
|
||||||
|
|
@ -123,38 +163,39 @@ public partial class MainPageViewModel : ViewModelBase
|
||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
internal async Task Save()
|
internal async Task Save()
|
||||||
{
|
{
|
||||||
// No selection means "create a new post from the editor".
|
// The button is already disabled when the title is empty
|
||||||
// The server is the source of truth, so we POST without an id
|
// (see CanSave), but the test path (and any programmatic
|
||||||
// and let BlogApiController assign one. The local view-model
|
// ICommand.Execute) bypasses CanExecute, so we still
|
||||||
// is then rebound to the server-issued record.
|
// guard here. Better to no-op with a status message
|
||||||
if (SelectedPost is null)
|
// than to send a request the server will reject.
|
||||||
|
if (string.IsNullOrWhiteSpace(DraftTitle))
|
||||||
{
|
{
|
||||||
var draft = new BlogPost
|
StatusMessage = "Title is required.";
|
||||||
{
|
|
||||||
Title = string.Empty,
|
|
||||||
Article = string.Empty,
|
|
||||||
DateCreated = DateTime.UtcNow,
|
|
||||||
DateModified = DateTime.UtcNow
|
|
||||||
};
|
|
||||||
await ExecuteAsync(async () =>
|
|
||||||
{
|
|
||||||
var created = await BlogClient.CreatePostAsync(draft);
|
|
||||||
if (created is not null)
|
|
||||||
{
|
|
||||||
SelectedPost = created;
|
|
||||||
StatusMessage = $"Created post {created.Id}.";
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await ExecuteAsync(async () =>
|
await ExecuteAsync(async () =>
|
||||||
{
|
{
|
||||||
if (SelectedPost.Id == 0)
|
// Build a fresh BlogPost 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)
|
||||||
{
|
{
|
||||||
SelectedPost.DateCreated = DateTime.UtcNow;
|
var draft = new BlogPost
|
||||||
SelectedPost.DateModified = DateTime.UtcNow;
|
{
|
||||||
var created = await BlogClient.CreatePostAsync(SelectedPost);
|
Title = DraftTitle,
|
||||||
|
Article = DraftArticle ?? string.Empty,
|
||||||
|
DateCreated = DateTime.UtcNow,
|
||||||
|
DateModified = DateTime.UtcNow,
|
||||||
|
};
|
||||||
|
var created = await BlogClient.CreatePostAsync(draft);
|
||||||
if (created is not null)
|
if (created is not null)
|
||||||
{
|
{
|
||||||
SelectedPost = created;
|
SelectedPost = created;
|
||||||
|
|
@ -163,8 +204,17 @@ public partial class MainPageViewModel : ViewModelBase
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
SelectedPost.DateModified = DateTime.UtcNow;
|
var update = new BlogPost
|
||||||
await BlogClient.UpdatePostAsync(SelectedPost.Id, SelectedPost);
|
{
|
||||||
|
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}.";
|
StatusMessage = $"Saved post {SelectedPost.Id}.";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -256,6 +306,14 @@ public partial class MainPageViewModel : ViewModelBase
|
||||||
DeleteCommand.NotifyCanExecuteChanged();
|
DeleteCommand.NotifyCanExecuteChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool CanSave() => SelectedPost is not null && !IsBusy;
|
/// <summary>Save is enabled as soon as the user has typed
|
||||||
|
/// a non-whitespace title in the editor, regardless of
|
||||||
|
/// whether a post is selected. The "no selection" case is
|
||||||
|
/// the create-new-post path; the "with selection" case is
|
||||||
|
/// the update path. Both read from the editor buffer.
|
||||||
|
/// Previously this also required <c>SelectedPost is not null</c>
|
||||||
|
/// — which contradicted the create-new-post intent and
|
||||||
|
/// forced the buggy "draft with empty title" branch.</summary>
|
||||||
|
private bool CanSave() => !IsBusy && !string.IsNullOrWhiteSpace(DraftTitle);
|
||||||
private bool CanDelete() => SelectedPost is not null && SelectedPost.Id != 0 && !IsBusy;
|
private bool CanDelete() => SelectedPost is not null && SelectedPost.Id != 0 && !IsBusy;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -79,9 +79,9 @@
|
||||||
</Grid.ColumnDefinitions>
|
</Grid.ColumnDefinitions>
|
||||||
|
|
||||||
<TextBlock Grid.Row="0" Text="Post detail" FontWeight="SemiBold" />
|
<TextBlock Grid.Row="0" Text="Post detail" FontWeight="SemiBold" />
|
||||||
<TextBox Grid.Row="1" Text="{Binding SelectedPost.Title, Mode=TwoWay}" PlaceholderText="Title" />
|
<TextBox Grid.Row="1" Text="{Binding DraftTitle, Mode=TwoWay}" PlaceholderText="Title" />
|
||||||
<AvaloniaEdit:TextEditor Grid.Row="2"
|
<AvaloniaEdit:TextEditor Grid.Row="2"
|
||||||
views:TextEditorBinding.Text="{Binding SelectedPost.Article, Mode=TwoWay}"
|
views:TextEditorBinding.Text="{Binding DraftArticle, Mode=TwoWay}"
|
||||||
ShowLineNumbers="True"
|
ShowLineNumbers="True"
|
||||||
FontFamily="Cascadia Code, Consolas, Menlo, Monospace"
|
FontFamily="Cascadia Code, Consolas, Menlo, Monospace"
|
||||||
MinHeight="320"
|
MinHeight="320"
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue