fix(card-editor): confirming an empty new card creates nothing

PLAN 5.4 says an unchanged confirmation writes nothing, but the shortcut
applied only to existing cards, so OK on a blank new-card editor added an
empty card. A new card with no title, content or tags is now not created.
This commit is contained in:
doyaGu committed 2026-10-03 09:12:36 -04:00
1 parent 4a88033695
commit ce6d9d2f83
4 files changed
+52 -8

No files matched your search

@@ -225,6 +225,48 @@ public class BoardViewModelTests
Assert.AreEqual(100, fixture.Repository.GetCard(card.Id)!.UpdatedAt);
}
[TestMethod]
public void ConfirmingAnEmptyNewCardCreatesNothing()
{
using var fixture = new Fixture();
ColumnModel column = fixture.Repository.AddColumn("A", "");
var editor = CardEditorDialogViewModel.ForNewCard(fixture.Repository, fixture.Dialogs, column.Id, confirmDiscard: true);
bool? closedWith = null;
editor.CloseRequested += (_, result) => closedWith = result;
editor.CardTitle = "draft";
editor.CardTitle = "";
editor.ConfirmCommand.Execute(null);
Assert.IsTrue(closedWith);
Assert.IsNull(editor.SavedCard);
Assert.AreEqual(0, fixture.Repository.GetCards().Count);
}
[TestMethod]
public async Task NewCardWithOnlyATagIsCreated()
{
using var fixture = new Fixture();
ColumnModel column = fixture.Repository.AddColumn("A", "");
TagModel bug = fixture.Repository.AddTag("bug", new RgbColor(255, 0, 0), "");
var editor = CardEditorDialogViewModel.ForNewCard(fixture.Repository, fixture.Dialogs, column.Id, confirmDiscard: true);
fixture.Dialogs.Script = dialog =>
{
if (dialog is TagPickerDialogViewModel picker)
{
picker.PickCommand.Execute(picker.FilteredTags.Single());
}
return Task.CompletedTask;
};
await editor.AddTagCommand.ExecuteAsync(null);
editor.ConfirmCommand.Execute(null);
CardModel card = fixture.Repository.GetCards().Single();
Assert.AreEqual("", card.Title);
Assert.AreEqual(bug.Id, card.Tags.Single().Id);
}
[TestMethod]
public async Task DirtyCancelAsksAndStaysOpenWhenDeclined()
{
@@ -12,8 +12,8 @@ namespace YKanBan.ViewModels.Dialogs;
/// The card editor — one unit of work. Title, content and the tag assignment
/// are edited in memory and written together in a single transaction on
/// confirmation; cancelling writes nothing about the card or its assignment.
/// Confirming an existing card without effective changes writes nothing and
/// keeps its timestamps. The only immediate write is the creation of a new tag
/// Confirming without effective changes writes nothing: an existing card keeps
/// its timestamps, and a new card left empty is not created. The only immediate write is the creation of a new tag
/// in the nested tag picker (see <see cref="TagPickerDialogViewModel"/>).
/// </summary>
public sealed partial class CardEditorDialogViewModel : DialogViewModelBase
@@ -113,7 +113,8 @@ public sealed partial class CardEditorDialogViewModel : DialogViewModelBase
public ObservableCollection<TagBadgeViewModel> Tags { get; }
/// <summary>
/// Gets the card written on confirmation; <see langword="null"/> until then.
/// Gets the card written on confirmation; <see langword="null"/> until then,
/// and after confirming a new card that was left empty.
/// </summary>
public CardModel? SavedCard { get; private set; }
@@ -182,13 +183,13 @@ public sealed partial class CardEditorDialogViewModel : DialogViewModelBase
private void RemoveTag(TagBadgeViewModel tag) => Tags.Remove(tag);
/// <summary>
/// Writes the card in one transaction and closes; an unchanged existing card
/// is not written at all.
/// Writes the card in one transaction and closes; an unchanged card is not
/// written at all, so a new card left empty is not created.
/// </summary>
[RelayCommand]
private void Confirm()
{
if (_existing is not null && !IsDirty)
if (!IsDirty)
{
SavedCard = _existing;
Close(true);
+1
View File
@@ -64,6 +64,7 @@
- [ ] 新建列、编辑列(标题 + 描述);空标题、重名(含仅大小写不同,如 `Done` / `done`)→ 模态内提示错误,模态不关、输入保留
- [ ] 删除列:确认框提示将删除 N 张卡片;确认后列与卡片一并删除
- [ ] 新建卡片、点击卡片编辑:标题可空;内容多行;`#id` / 创建时间 / 修改时间只读显示(新建时不显示)
- [ ] 新建卡片什么都不填(标题、内容、标签均空)直接确认:不产生卡片;只加一个标签再确认:产生无标题卡片
- [ ] 编辑确认但内容未变 → 修改时间不变;改动后确认 → 修改时间刷新
- [ ] 有未保存改动时取消或点标题栏关闭 → 丢弃确认(开关开启时)
- [ ] 卡片菜单"移动到列":卡片移动,修改时间**不**变
+2 -2
View File
@@ -16,7 +16,7 @@
- 网络共享明确不支持;命令行参数边界;macOS 无参启动行为写入文档(§2.3、§4.1、附录)
- 补充:导出转义规则、卡片列表虚拟化、模态窗口 FlowDirection、`Resources` 类名使用规则、CI osx-x64 运行器(§5、§7、§8)
- 依赖:Avalonia 11.3.6 → 11.3.22,Semi.Avalonia 11.2.1.10 → 11.3.22(§7)
- 实现回补:搜索括号嵌套上限 64 层、单个表达式至多 256 个原子,空短语、短语紧贴文本、`key:` 后空白均为非法(§6.1、§6.2);导出表格单元格转义 `\`、默认文件名剔除非法字符(§5.8);初始化中途锁冲突进锁冲突页、数据库文件缺失不静默新建(§4.4);app.json 读不出 / 备份写不出 / 退出写盘失败均不阻断启动与退出(§4.3)
- 实现回补:搜索括号嵌套上限 64 层、单个表达式至多 256 个原子,空短语、短语紧贴文本、`key:` 后空白均为非法(§6.1、§6.2);导出表格单元格转义 `\`、默认文件名剔除非法字符(§5.8);初始化中途锁冲突进锁冲突页、数据库文件缺失不静默新建(§4.4);app.json 读不出 / 备份写不出 / 退出写盘失败均不阻断启动与退出(§4.3);主题下拉选中即预览、取消恢复(§5.6);空白新卡片确认不创建(§5.4)
- 里程碑:标注 M0–M4 已完成,新增 **M4R(v1.9 回补)**(§9)
## 1. 项目概述
@@ -201,7 +201,7 @@ card_tags: (card_id, tag_id) 复合主键,双向 ON DELETE CASCADE
- 字段:标题(可空,空串 = 无标题)、内容(多行)、标签区、只读 `#id` / 创建时间 / 修改时间(新增时未生成则不显示)
- 底部:确认(当前数据一次性写入数据库)/ 取消(丢弃更改)
- 编辑对话框是一个工作单元:确认时一次性落库(单事务,见 §3),取消不产生任何卡片/指派写入;其余操作(删除、移动等)仍即时写库
- 确认时内容无变化 → 不写库、不刷新时间戳
- 确认时内容无变化 → 不写库、不刷新时间戳;新建卡片时即标题、内容、标签均为空 → 确认不创建卡片(等同取消)
- 标签指派:编辑模态标签区显示已指派标签(各带 × 移除),"添加标签"按钮弹**嵌套模态** = 已有标签搜索/列表 + 新建标签入口,选中即添加,可反复打开继续添加下一个
- **例外**:嵌套模态中新建的标签**立即写入 `tags` 表**(标签是全局实体);外层编辑取消时仅丢弃指派关系,新建的标签保留(与"tag 永不自动清理"一致)