From ce6d9d2f83e019b47dcef8deec89df98ecb610c5 Mon Sep 17 00:00:00 2001 From: doyaGu Date: Sat, 3 Oct 2026 09:12:36 -0400 Subject: [PATCH] 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. --- .../ViewModels/BoardViewModelTests.cs | 42 +++++++++++++++++++ .../Dialogs/CardEditorDialogViewModel.cs | 13 +++--- docs/ACCEPTANCE.md | 1 + docs/PLAN.md | 4 +- 4 files changed, 52 insertions(+), 8 deletions(-) diff --git a/YKanBan.Tests/ViewModels/BoardViewModelTests.cs b/YKanBan.Tests/ViewModels/BoardViewModelTests.cs index 9f3c80f..8acc66e 100644 --- a/YKanBan.Tests/ViewModels/BoardViewModelTests.cs +++ b/YKanBan.Tests/ViewModels/BoardViewModelTests.cs @@ -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() { diff --git a/YKanBan/ViewModels/Dialogs/CardEditorDialogViewModel.cs b/YKanBan/ViewModels/Dialogs/CardEditorDialogViewModel.cs index 5c7a11e..562120b 100644 --- a/YKanBan/ViewModels/Dialogs/CardEditorDialogViewModel.cs +++ b/YKanBan/ViewModels/Dialogs/CardEditorDialogViewModel.cs @@ -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 ). /// public sealed partial class CardEditorDialogViewModel : DialogViewModelBase @@ -113,7 +113,8 @@ public sealed partial class CardEditorDialogViewModel : DialogViewModelBase public ObservableCollection Tags { get; } /// - /// Gets the card written on confirmation; until then. + /// Gets the card written on confirmation; until then, + /// and after confirming a new card that was left empty. /// public CardModel? SavedCard { get; private set; } @@ -182,13 +183,13 @@ public sealed partial class CardEditorDialogViewModel : DialogViewModelBase private void RemoveTag(TagBadgeViewModel tag) => Tags.Remove(tag); /// - /// 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. /// [RelayCommand] private void Confirm() { - if (_existing is not null && !IsDirty) + if (!IsDirty) { SavedCard = _existing; Close(true); diff --git a/docs/ACCEPTANCE.md b/docs/ACCEPTANCE.md index 43220d3..153e717 100644 --- a/docs/ACCEPTANCE.md +++ b/docs/ACCEPTANCE.md @@ -64,6 +64,7 @@ - [ ] 新建列、编辑列(标题 + 描述);空标题、重名(含仅大小写不同,如 `Done` / `done`)→ 模态内提示错误,模态不关、输入保留 - [ ] 删除列:确认框提示将删除 N 张卡片;确认后列与卡片一并删除 - [ ] 新建卡片、点击卡片编辑:标题可空;内容多行;`#id` / 创建时间 / 修改时间只读显示(新建时不显示) +- [ ] 新建卡片什么都不填(标题、内容、标签均空)直接确认:不产生卡片;只加一个标签再确认:产生无标题卡片 - [ ] 编辑确认但内容未变 → 修改时间不变;改动后确认 → 修改时间刷新 - [ ] 有未保存改动时取消或点标题栏关闭 → 丢弃确认(开关开启时) - [ ] 卡片菜单"移动到列":卡片移动,修改时间**不**变 diff --git a/docs/PLAN.md b/docs/PLAN.md index 5c1988a..b2839bd 100644 --- a/docs/PLAN.md +++ b/docs/PLAN.md @@ -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 永不自动清理"一致)