feat(data): add TaskRepository.SetTagsAsync for full tag-set replacement
This commit is contained in:
@@ -194,6 +194,27 @@ public sealed class TaskRepository
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task SetTagsAsync(string taskId, IReadOnlyList<string> tagNames, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
var task = await _context.Tasks.Include(t => t.Tags).FirstOrDefaultAsync(t => t.Id == taskId, ct);
|
||||||
|
if (task is null) return;
|
||||||
|
|
||||||
|
task.Tags.Clear();
|
||||||
|
|
||||||
|
foreach (var name in tagNames.Where(n => !string.IsNullOrWhiteSpace(n)).Distinct(StringComparer.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
var tag = await _context.Tags.FirstOrDefaultAsync(t => t.Name == name, ct);
|
||||||
|
if (tag is null)
|
||||||
|
{
|
||||||
|
tag = new TagEntity { Name = name };
|
||||||
|
_context.Tags.Add(tag);
|
||||||
|
}
|
||||||
|
task.Tags.Add(tag);
|
||||||
|
}
|
||||||
|
|
||||||
|
await _context.SaveChangesAsync(ct);
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<List<TagEntity>> GetTagsAsync(string taskId, CancellationToken ct = default)
|
public async Task<List<TagEntity>> GetTagsAsync(string taskId, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
return await _context.Tasks
|
return await _context.Tasks
|
||||||
@@ -276,6 +297,41 @@ public sealed class TaskRepository
|
|||||||
return child;
|
return child;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task UpdateChildAsync(
|
||||||
|
string taskId,
|
||||||
|
string? title,
|
||||||
|
string? description,
|
||||||
|
string? commitType,
|
||||||
|
IReadOnlyList<string>? tagNames,
|
||||||
|
TaskStatus? status,
|
||||||
|
CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
var task = await _context.Tasks.Include(t => t.Tags).FirstOrDefaultAsync(t => t.Id == taskId, ct)
|
||||||
|
?? throw new InvalidOperationException($"Task {taskId} not found.");
|
||||||
|
|
||||||
|
if (title is not null) task.Title = title;
|
||||||
|
if (description is not null) task.Description = description;
|
||||||
|
if (commitType is not null) task.CommitType = commitType;
|
||||||
|
if (status.HasValue) task.Status = status.Value;
|
||||||
|
|
||||||
|
if (tagNames is not null)
|
||||||
|
{
|
||||||
|
task.Tags.Clear();
|
||||||
|
foreach (var name in tagNames.Where(n => !string.IsNullOrWhiteSpace(n)).Distinct(StringComparer.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
var tag = await _context.Tags.FirstOrDefaultAsync(t => t.Name == name, ct);
|
||||||
|
if (tag is null)
|
||||||
|
{
|
||||||
|
tag = new TagEntity { Name = name };
|
||||||
|
_context.Tags.Add(tag);
|
||||||
|
}
|
||||||
|
task.Tags.Add(tag);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await _context.SaveChangesAsync(ct);
|
||||||
|
}
|
||||||
|
|
||||||
public async Task UpdatePlanningTaskAsync(
|
public async Task UpdatePlanningTaskAsync(
|
||||||
string taskId,
|
string taskId,
|
||||||
string? title,
|
string? title,
|
||||||
|
|||||||
@@ -340,4 +340,60 @@ public sealed class TaskRepositoryTests : IDisposable
|
|||||||
Assert.Contains("code", names);
|
Assert.Contains("code", names);
|
||||||
Assert.Contains("manual", names);
|
Assert.Contains("manual", names);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task SetTagsAsync_AttachesNewTagsAndCreatesMissingRows()
|
||||||
|
{
|
||||||
|
var listId = await CreateListAsync("L");
|
||||||
|
var task = MakeTask(listId);
|
||||||
|
await _tasks.AddAsync(task);
|
||||||
|
|
||||||
|
await _tasks.SetTagsAsync(task.Id, new[] { "agent", "novel-tag" });
|
||||||
|
|
||||||
|
var tags = await _tasks.GetTagsAsync(task.Id);
|
||||||
|
Assert.Contains(tags, t => t.Name == "agent");
|
||||||
|
Assert.Contains(tags, t => t.Name == "novel-tag");
|
||||||
|
Assert.Equal(2, tags.Count);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task SetTagsAsync_ReplacesExistingTagSet()
|
||||||
|
{
|
||||||
|
var listId = await CreateListAsync("L");
|
||||||
|
var task = MakeTask(listId);
|
||||||
|
await _tasks.AddAsync(task);
|
||||||
|
await _tasks.SetTagsAsync(task.Id, new[] { "agent" });
|
||||||
|
|
||||||
|
await _tasks.SetTagsAsync(task.Id, new[] { "manual" });
|
||||||
|
|
||||||
|
var tags = await _tasks.GetTagsAsync(task.Id);
|
||||||
|
Assert.Single(tags);
|
||||||
|
Assert.Equal("manual", tags[0].Name);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task SetTagsAsync_DeduplicatesCaseInsensitively()
|
||||||
|
{
|
||||||
|
var listId = await CreateListAsync("L");
|
||||||
|
var task = MakeTask(listId);
|
||||||
|
await _tasks.AddAsync(task);
|
||||||
|
|
||||||
|
await _tasks.SetTagsAsync(task.Id, new[] { "agent", "AGENT", "Agent" });
|
||||||
|
|
||||||
|
var tags = await _tasks.GetTagsAsync(task.Id);
|
||||||
|
Assert.Single(tags);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task SetTagsAsync_EmptyListClearsAllTags()
|
||||||
|
{
|
||||||
|
var listId = await CreateListAsync("L");
|
||||||
|
var task = MakeTask(listId);
|
||||||
|
await _tasks.AddAsync(task);
|
||||||
|
await _tasks.SetTagsAsync(task.Id, new[] { "agent" });
|
||||||
|
|
||||||
|
await _tasks.SetTagsAsync(task.Id, Array.Empty<string>());
|
||||||
|
|
||||||
|
Assert.Empty(await _tasks.GetTagsAsync(task.Id));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user