feat(ui): Ticket-Projekt pro Liste und Import ueber das Kontextmenue
This commit is contained in:
@@ -208,12 +208,12 @@ public sealed partial class AgentConfigEditorViewModel : ViewModelBase, IDisposa
|
||||
catch { }
|
||||
}
|
||||
|
||||
// verifyCommand and serializeOnFileOverlap are List-only fields owned by
|
||||
// verifyCommand, serializeOnFileOverlap and ticketProjectId are List-only fields owned by
|
||||
// ListSettingsModalViewModel (not this editor, which is also reused for Task scope); the caller
|
||||
// passes them through so the single UpdateListConfig call carries the full desired row instead
|
||||
// of clobbering it.
|
||||
public async System.Threading.Tasks.Task SaveAsync(
|
||||
string? verifyCommand = null, bool? serializeOnFileOverlap = null)
|
||||
string? verifyCommand = null, bool? serializeOnFileOverlap = null, int? ticketProjectId = null)
|
||||
{
|
||||
if (TargetId is null) return;
|
||||
var model = string.IsNullOrWhiteSpace(Model) ? null : Model;
|
||||
@@ -226,7 +226,7 @@ public sealed partial class AgentConfigEditorViewModel : ViewModelBase, IDisposa
|
||||
if (_scope == AgentConfigScope.Task)
|
||||
await _worker.UpdateTaskAgentSettingsAsync(new UpdateTaskAgentSettingsDto(TargetId, model, sp, ap, turns, skills, permission));
|
||||
else
|
||||
await _worker.UpdateListConfigAsync(new UpdateListConfigDto(TargetId, model, sp, ap, turns, skills, verifyCommand, serializeOnFileOverlap, permission));
|
||||
await _worker.UpdateListConfigAsync(new UpdateListConfigDto(TargetId, model, sp, ap, turns, skills, verifyCommand, serializeOnFileOverlap, permission, ticketProjectId));
|
||||
}
|
||||
|
||||
private List<string>? SelectedSessionSkillNames()
|
||||
|
||||
@@ -16,6 +16,9 @@ public sealed partial class ListNavItemViewModel : ViewModelBase
|
||||
[ObservableProperty] private bool _isManual;
|
||||
// Whether this list's .claudedo/ findings folder is committed with the repo (ListEntity.FindingsTracked).
|
||||
[ObservableProperty] private bool _findingsTracked;
|
||||
// True when a ticket-system base URL is configured AND this list has a linked ticket project
|
||||
// (ListConfigEntity.TicketProjectId) — gates the "Update from ticket system" context menu entry.
|
||||
[ObservableProperty] private bool _hasTicketProject;
|
||||
[ObservableProperty] private bool _dropHintAbove;
|
||||
[ObservableProperty] private bool _dropHintBelow;
|
||||
// Set while a dragged task hovers over this row as a move target — distinct from
|
||||
|
||||
@@ -285,6 +285,7 @@ public sealed partial class ListsIslandViewModel : ViewModelBase, IDisposable
|
||||
var ok = false;
|
||||
try
|
||||
{
|
||||
var ticketsConfigured = await IsTicketsConfiguredAsync();
|
||||
await using var ctx = await _dbFactory.CreateDbContextAsync(ct);
|
||||
var lists = new ListRepository(ctx);
|
||||
var seedNames = new HashSet<string>(new[] { "My Day", "Important", "Planned" });
|
||||
@@ -293,6 +294,7 @@ public sealed partial class ListsIslandViewModel : ViewModelBase, IDisposable
|
||||
foreach (var l in await lists.GetAllAsync(ct))
|
||||
{
|
||||
if (seedNames.Contains(l.Name)) continue;
|
||||
var cfg = await lists.GetConfigAsync(l.Id, ct);
|
||||
var item = new ListNavItemViewModel
|
||||
{
|
||||
Id = $"user:{l.Id}",
|
||||
@@ -304,6 +306,7 @@ public sealed partial class ListsIslandViewModel : ViewModelBase, IDisposable
|
||||
DefaultCommitType = l.DefaultCommitType,
|
||||
IsManual = l.IsManual,
|
||||
FindingsTracked = l.FindingsTracked,
|
||||
HasTicketProject = ticketsConfigured && cfg?.TicketProjectId > 0,
|
||||
};
|
||||
Items.Add(item);
|
||||
UserLists.Add(item);
|
||||
@@ -486,10 +489,12 @@ public sealed partial class ListsIslandViewModel : ViewModelBase, IDisposable
|
||||
var lists = new ListRepository(ctx);
|
||||
var entity = await lists.GetByIdAsync(rawId);
|
||||
if (entity is null) return;
|
||||
var cfg = await lists.GetConfigAsync(rawId);
|
||||
var hasTicketProject = await IsTicketsConfiguredAsync() && cfg?.TicketProjectId > 0;
|
||||
|
||||
if (row is null)
|
||||
{
|
||||
await AddRowAsync(lists, entity);
|
||||
await AddRowAsync(lists, entity, hasTicketProject);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -498,16 +503,47 @@ public sealed partial class ListsIslandViewModel : ViewModelBase, IDisposable
|
||||
row.DefaultCommitType = entity.DefaultCommitType;
|
||||
row.IsManual = entity.IsManual;
|
||||
row.FindingsTracked = entity.FindingsTracked;
|
||||
row.HasTicketProject = hasTicketProject;
|
||||
RecomputeHasNoLinkedRepo();
|
||||
}
|
||||
catch { /* best-effort refresh */ }
|
||||
}
|
||||
|
||||
private async Task<bool> IsTicketsConfiguredAsync()
|
||||
{
|
||||
if (_worker is null) return false;
|
||||
try
|
||||
{
|
||||
var settings = await _worker.GetTicketSettingsAsync();
|
||||
return !string.IsNullOrWhiteSpace(settings?.ApiBaseUrl);
|
||||
}
|
||||
catch { return false; }
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task ImportTicketsAsync(ListNavItemViewModel? row)
|
||||
{
|
||||
if (row is null || _worker is null) return;
|
||||
var rawId = row.Id.StartsWith("user:", StringComparison.Ordinal) ? row.Id["user:".Length..] : row.Id;
|
||||
try
|
||||
{
|
||||
var result = await _worker.ImportTicketsAsync(rawId);
|
||||
// No FlashFooterInfo variant exists in this project — the footer strip only has an
|
||||
// error channel (ErrorReported -> IslandsShellViewModel.FlashFooterError). A silent
|
||||
// success is the worse option, so the result reuses the same channel.
|
||||
ErrorReported?.Invoke(Loc.T("lists.importTicketsResult", result.Examined, result.Created));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ErrorReported?.Invoke(ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A `create_list` from a running MCP session has no local row yet — build one from the DB
|
||||
/// entity and insert it at its DB sort position, instead of waiting for the next full reload.
|
||||
/// </summary>
|
||||
private async Task AddRowAsync(ListRepository lists, ListEntity entity)
|
||||
private async Task AddRowAsync(ListRepository lists, ListEntity entity, bool hasTicketProject = false)
|
||||
{
|
||||
var dotColors = new[] { "Moss", "Peat", "Sage" };
|
||||
var item = new ListNavItemViewModel
|
||||
@@ -521,6 +557,7 @@ public sealed partial class ListsIslandViewModel : ViewModelBase, IDisposable
|
||||
DefaultCommitType = entity.DefaultCommitType,
|
||||
IsManual = entity.IsManual,
|
||||
FindingsTracked = entity.FindingsTracked,
|
||||
HasTicketProject = hasTicketProject,
|
||||
};
|
||||
|
||||
var seedNames = new HashSet<string>(new[] { "My Day", "Important", "Planned" });
|
||||
|
||||
@@ -11,6 +11,10 @@ using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace ClaudeDo.Ui.ViewModels.Modals;
|
||||
|
||||
// UI-only projection of TicketProjectDto for the ComboBox display text, plus a synthetic
|
||||
// Id=0 "none" entry (mirrors AgentConfigEditorViewModel's "(none)" AgentInfo placeholder).
|
||||
public sealed record TicketProjectOption(int Id, string Display);
|
||||
|
||||
public sealed partial class ListSettingsModalViewModel : ViewModelBase
|
||||
{
|
||||
private readonly IWorkerClient _worker;
|
||||
@@ -39,6 +43,10 @@ public sealed partial class ListSettingsModalViewModel : ViewModelBase
|
||||
// When on, the queue picker holds back a queued task whose declared scope globs overlap a
|
||||
// running/awaiting-merge sibling in this list (ListConfigEntity.SerializeOnFileOverlap).
|
||||
[ObservableProperty] private bool _serializeOnFileOverlap;
|
||||
// Dropdown hidden entirely when no ticket-system base URL is configured (Settings → Files tab).
|
||||
[ObservableProperty] private bool _ticketsAvailable;
|
||||
[ObservableProperty] private TicketProjectOption? _selectedTicketProject;
|
||||
public ObservableCollection<TicketProjectOption> TicketProjects { get; } = new();
|
||||
|
||||
public ObservableCollection<string> CommitTypeOptions { get; } = new(CommitTypeRegistry.Types);
|
||||
|
||||
@@ -75,6 +83,24 @@ public sealed partial class ListSettingsModalViewModel : ViewModelBase
|
||||
var cfg = await _worker.GetListConfigAsync(listId);
|
||||
VerifyCommand = cfg?.VerifyCommand ?? "";
|
||||
SerializeOnFileOverlap = cfg?.SerializeOnFileOverlap ?? false;
|
||||
|
||||
var ticketSettings = await _worker.GetTicketSettingsAsync();
|
||||
TicketsAvailable = !string.IsNullOrWhiteSpace(ticketSettings?.ApiBaseUrl);
|
||||
TicketProjects.Clear();
|
||||
if (TicketsAvailable)
|
||||
{
|
||||
TicketProjects.Add(new TicketProjectOption(0, Loc.T("vm.listSettings.ticketProjectNone")));
|
||||
foreach (var p in await _worker.GetTicketProjectsAsync())
|
||||
TicketProjects.Add(new TicketProjectOption(p.Id, $"{p.DepartmentName} / {p.Title}"));
|
||||
|
||||
SelectedTicketProject = cfg?.TicketProjectId is int id
|
||||
? TicketProjects.FirstOrDefault(t => t.Id == id) ?? TicketProjects[0]
|
||||
: TicketProjects[0];
|
||||
}
|
||||
else
|
||||
{
|
||||
SelectedTicketProject = null;
|
||||
}
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
@@ -88,9 +114,15 @@ public sealed partial class ListSettingsModalViewModel : ViewModelBase
|
||||
IsManual,
|
||||
FindingsTracked));
|
||||
|
||||
// Tri-state: dropdown never shown (no base URL) -> null, leave the stored link alone.
|
||||
// Dropdown shown -> always an explicit value, 0 for "— none —" to actively clear it.
|
||||
// Never null here, or an explicit unlink would silently fail to reach the worker.
|
||||
var ticketProjectId = TicketsAvailable ? (SelectedTicketProject?.Id ?? 0) : (int?)null;
|
||||
|
||||
await Agent.SaveAsync(
|
||||
string.IsNullOrWhiteSpace(VerifyCommand) ? null : VerifyCommand,
|
||||
SerializeOnFileOverlap);
|
||||
SerializeOnFileOverlap,
|
||||
ticketProjectId);
|
||||
|
||||
CloseAction?.Invoke();
|
||||
}
|
||||
|
||||
@@ -155,6 +155,10 @@
|
||||
IsVisible="{Binding WorkingDir, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"
|
||||
Command="{Binding $parent[UserControl].((vm:ListsIslandViewModel)DataContext).OpenInTerminalCommand}"
|
||||
CommandParameter="{Binding}"/>
|
||||
<MenuItem Header="{loc:Tr lists.contextImportTickets}"
|
||||
IsVisible="{Binding HasTicketProject}"
|
||||
Command="{Binding $parent[UserControl].((vm:ListsIslandViewModel)DataContext).ImportTicketsCommand}"
|
||||
CommandParameter="{Binding}"/>
|
||||
<Separator/>
|
||||
<MenuItem Header="{loc:Tr lists.contextSettings}"
|
||||
Command="{Binding $parent[UserControl].((vm:ListsIslandViewModel)DataContext).OpenListSettingsCommand}"
|
||||
|
||||
@@ -81,6 +81,14 @@
|
||||
<TextBlock Text="{loc:Tr modals.listSettings.serializeOnFileOverlapHint}"
|
||||
Opacity="0.6" FontSize="12" TextWrapping="Wrap"/>
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel Spacing="4" IsVisible="{Binding TicketsAvailable}">
|
||||
<TextBlock Classes="field-label" Text="{loc:Tr modals.listSettings.ticketProject}"/>
|
||||
<ComboBox ItemsSource="{Binding TicketProjects}"
|
||||
SelectedItem="{Binding SelectedTicketProject, Mode=TwoWay}"
|
||||
DisplayMemberBinding="{Binding Display}"
|
||||
HorizontalAlignment="Left" MinWidth="240"/>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
|
||||
Reference in New Issue
Block a user