fix(board): skip the refresh when the card editor wrote nothing
Every editor close rebuilt all columns, so opening a card and cancelling reset each column's scroll position. The editor now reports whether it saved the card or created a tag in the nested picker, and the board only refreshes in that case.
This commit is contained in:
1 parent
a2c4e1853d
commit
253847323a
5 files changed
+91
-6
No files matched your search
@@ -204,6 +204,66 @@ public class BoardViewModelTests
|
|||||||
Assert.AreEqual(card.Id, board.Columns[0].Cards.Single().Card.Id);
|
Assert.AreEqual(card.Id, board.Columns[0].Cards.Single().Card.Id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public async Task EditorThatWroteNothingLeavesTheColumnsAlone()
|
||||||
|
{
|
||||||
|
using var fixture = new Fixture();
|
||||||
|
ColumnModel column = fixture.Repository.AddColumn("A", "");
|
||||||
|
fixture.Repository.AddCard(column.Id, "t", "", []);
|
||||||
|
BoardViewModel board = fixture.CreateBoard();
|
||||||
|
ColumnViewModel shown = board.Columns.Single();
|
||||||
|
int writes = 0;
|
||||||
|
board.DataWritten += (_, _) => writes++;
|
||||||
|
|
||||||
|
// Cancel a new card, confirm an unchanged existing card, confirm an empty new card.
|
||||||
|
fixture.Dialogs.Script = dialog => dialog is CardEditorDialogViewModel editor
|
||||||
|
? editor.RequestCancelAsync()
|
||||||
|
: Task.CompletedTask;
|
||||||
|
await shown.AddCardCommand.ExecuteAsync(null);
|
||||||
|
fixture.Dialogs.Script = dialog =>
|
||||||
|
{
|
||||||
|
(dialog as CardEditorDialogViewModel)?.ConfirmCommand.Execute(null);
|
||||||
|
return Task.CompletedTask;
|
||||||
|
};
|
||||||
|
await shown.Cards.Single().EditCommand.ExecuteAsync(null);
|
||||||
|
await shown.AddCardCommand.ExecuteAsync(null);
|
||||||
|
|
||||||
|
Assert.AreSame(shown, board.Columns.Single());
|
||||||
|
Assert.AreEqual(0, writes);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public async Task TagCreatedInACancelledEditRefreshesTheBoard()
|
||||||
|
{
|
||||||
|
using var fixture = new Fixture();
|
||||||
|
fixture.Repository.AddColumn("A", "");
|
||||||
|
BoardViewModel board = fixture.CreateBoard();
|
||||||
|
ColumnViewModel shown = board.Columns.Single();
|
||||||
|
int writes = 0;
|
||||||
|
board.DataWritten += (_, _) => writes++;
|
||||||
|
fixture.Dialogs.Script = async dialog =>
|
||||||
|
{
|
||||||
|
switch (dialog)
|
||||||
|
{
|
||||||
|
case CardEditorDialogViewModel editor:
|
||||||
|
await editor.AddTagCommand.ExecuteAsync(null);
|
||||||
|
editor.Tags.Clear();
|
||||||
|
await editor.RequestCancelAsync();
|
||||||
|
break;
|
||||||
|
case TagPickerDialogViewModel picker:
|
||||||
|
picker.NewName = "fresh";
|
||||||
|
picker.NewColor = "#123456";
|
||||||
|
picker.CreateCommand.Execute(null);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
await shown.AddCardCommand.ExecuteAsync(null);
|
||||||
|
|
||||||
|
Assert.AreNotSame(shown, board.Columns.Single());
|
||||||
|
Assert.AreEqual(1, writes);
|
||||||
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
public void UnchangedEditConfirmationWritesNothing()
|
public void UnchangedEditConfirmationWritesNothing()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -274,9 +274,7 @@ public sealed partial class BoardViewModel : ViewModelBase
|
|||||||
var editor = CardEditorDialogViewModel.ForNewCard(
|
var editor = CardEditorDialogViewModel.ForNewCard(
|
||||||
_repository, _dialogs, columnId, _config.Confirmations.DiscardEdit);
|
_repository, _dialogs, columnId, _config.Confirmations.DiscardEdit);
|
||||||
await _dialogs.ShowAsync(editor);
|
await _dialogs.ShowAsync(editor);
|
||||||
|
await RefreshAfterEditorAsync(editor);
|
||||||
// Refresh even after a cancel: the nested picker may have created tags.
|
|
||||||
await RefreshAfterWriteAsync();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -306,9 +304,20 @@ public sealed partial class BoardViewModel : ViewModelBase
|
|||||||
var editor = CardEditorDialogViewModel.ForExistingCard(
|
var editor = CardEditorDialogViewModel.ForExistingCard(
|
||||||
_repository, _dialogs, card, _config.Confirmations.DiscardEdit);
|
_repository, _dialogs, card, _config.Confirmations.DiscardEdit);
|
||||||
await _dialogs.ShowAsync(editor);
|
await _dialogs.ShowAsync(editor);
|
||||||
await RefreshAfterWriteAsync();
|
await RefreshAfterEditorAsync(editor);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Refreshes after the card editor closes, but only when it wrote something:
|
||||||
|
/// a saved card, or a tag created in the nested picker even if the edit was
|
||||||
|
/// cancelled. An editor that wrote nothing leaves the columns, and so their
|
||||||
|
/// scroll positions, untouched.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="editor">The closed editor.</param>
|
||||||
|
/// <returns>A task completing after the refresh, if any.</returns>
|
||||||
|
private Task RefreshAfterEditorAsync(CardEditorDialogViewModel editor) =>
|
||||||
|
editor.WroteToWorkspace ? RefreshAfterWriteAsync() : Task.CompletedTask;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Deletes a card, asking first when that confirmation is enabled.
|
/// Deletes a card, asking first when that confirmation is enabled.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -118,6 +118,12 @@ public sealed partial class CardEditorDialogViewModel : DialogViewModelBase
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public CardModel? SavedCard { get; private set; }
|
public CardModel? SavedCard { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets whether this edit session wrote anything: the card on confirmation,
|
||||||
|
/// or a tag created in the nested picker (kept even when the edit is cancelled).
|
||||||
|
/// </summary>
|
||||||
|
public bool WroteToWorkspace { get; private set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the card title; the empty string means "no title".
|
/// Gets or sets the card title; the empty string means "no title".
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -169,9 +175,11 @@ public sealed partial class CardEditorDialogViewModel : DialogViewModelBase
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (await _dialogs.ShowAsync(picker) && picker.PickedTag is { } picked)
|
bool picked = await _dialogs.ShowAsync(picker);
|
||||||
|
WroteToWorkspace |= picker.CreatedTag;
|
||||||
|
if (picked && picker.PickedTag is { } tag)
|
||||||
{
|
{
|
||||||
Tags.Add(new TagBadgeViewModel(picked));
|
Tags.Add(new TagBadgeViewModel(tag));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -202,6 +210,7 @@ public sealed partial class CardEditorDialogViewModel : DialogViewModelBase
|
|||||||
SavedCard = _existing is null
|
SavedCard = _existing is null
|
||||||
? _repository.AddCard(_columnId, CardTitle, Content, tagIds)
|
? _repository.AddCard(_columnId, CardTitle, Content, tagIds)
|
||||||
: _repository.UpdateCard(_existing.Id, CardTitle, Content, tagIds);
|
: _repository.UpdateCard(_existing.Id, CardTitle, Content, tagIds);
|
||||||
|
WroteToWorkspace = true;
|
||||||
}
|
}
|
||||||
catch (Exception exception)
|
catch (Exception exception)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -48,6 +48,11 @@ public sealed partial class TagPickerDialogViewModel : DialogViewModelBase
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public TagModel? PickedTag { get; private set; }
|
public TagModel? PickedTag { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets whether <see cref="PickedTag"/> was newly created, i.e. written to the workspace.
|
||||||
|
/// </summary>
|
||||||
|
public bool CreatedTag { get; private set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the available tags matching <see cref="FilterText"/>.
|
/// Gets the available tags matching <see cref="FilterText"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -127,6 +132,7 @@ public sealed partial class TagPickerDialogViewModel : DialogViewModelBase
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
PickedTag = _repository.AddTag(NewName, color, NewDescription);
|
PickedTag = _repository.AddTag(NewName, color, NewDescription);
|
||||||
|
CreatedTag = true;
|
||||||
}
|
}
|
||||||
catch (Exception exception)
|
catch (Exception exception)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -75,6 +75,7 @@
|
|||||||
- [ ] 列宽:窄 240 / 标准 320 / 宽 480 / 手动 160–720;全局生效,重启保持
|
- [ ] 列宽:窄 240 / 标准 320 / 宽 480 / 手动 160–720;全局生效,重启保持
|
||||||
- [ ] 排序:id 升序 / 最后修改时间降序 / 标题升序(空标题在前);只影响卡片,列始终 id 序;重启保持
|
- [ ] 排序:id 升序 / 最后修改时间降序 / 标题升序(空标题在前);只影响卡片,列始终 id 序;重启保持
|
||||||
- [ ] 大数据量:用脚本向一列灌入约 10000 张卡片,滚动流畅、打开工作区无明显卡顿(虚拟化)
|
- [ ] 大数据量:用脚本向一列灌入约 10000 张卡片,滚动流畅、打开工作区无明显卡顿(虚拟化)
|
||||||
|
- [ ] 长列滚动到中部后打开某张卡片,不改动就取消或确认 → 各列滚动位置不变(未写库不刷新看板)
|
||||||
- [ ] 所有模态在父窗口居中;使用系统原生标题栏
|
- [ ] 所有模态在父窗口居中;使用系统原生标题栏
|
||||||
- [ ] 打开带输入框的模态时,第一个输入框自动获得焦点并全选
|
- [ ] 打开带输入框的模态时,第一个输入框自动获得焦点并全选
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user