feat(ui): collapsible folders for the list sidebar
Folders render above the top-level lists, each with its own collapsible children; tapping a folder header folds it instead of selecting it. Lists move in by dragging onto a folder header (or onto a row inside it) and out by dropping between top-level rows or via the context menu. Creating a folder is a toggle in the New-list modal, not a third sidebar button: it reduces the modal to name + icon and writes a folder row instead of the list row the create flow pre-created.
This commit is contained in:
@@ -260,6 +260,8 @@
|
|||||||
"contextOpenFindings": "Findings-Ordner öffnen",
|
"contextOpenFindings": "Findings-Ordner öffnen",
|
||||||
"contextImportTickets": "Aus Ticketsystem aktualisieren",
|
"contextImportTickets": "Aus Ticketsystem aktualisieren",
|
||||||
"contextSettings": "Einstellungen…",
|
"contextSettings": "Einstellungen…",
|
||||||
|
"contextFolderSettings": "Ordner-Einstellungen…",
|
||||||
|
"contextRemoveFromFolder": "Aus Ordner herausnehmen",
|
||||||
"newList": "Neue Liste",
|
"newList": "Neue Liste",
|
||||||
"addReposTip": "Repos als Listen hinzufügen",
|
"addReposTip": "Repos als Listen hinzufügen",
|
||||||
"noRepoBannerText": "Noch kein Repo verknüpft. ClaudeDo führt Tasks in einem Git-Repo aus.",
|
"noRepoBannerText": "Noch kein Repo verknüpft. ClaudeDo führt Tasks in einem Git-Repo aus.",
|
||||||
@@ -403,7 +405,11 @@
|
|||||||
},
|
},
|
||||||
"listSettings": {
|
"listSettings": {
|
||||||
"title": "LISTENEINSTELLUNGEN",
|
"title": "LISTENEINSTELLUNGEN",
|
||||||
|
"folderTitle": "ORDNER",
|
||||||
"deleteList": "Liste löschen",
|
"deleteList": "Liste löschen",
|
||||||
|
"deleteFolder": "Ordner löschen",
|
||||||
|
"createAsFolder": "Stattdessen einen Ordner anlegen",
|
||||||
|
"createAsFolderHint": "Ein Ordner gruppiert nur Listen in der Seitenleiste — er enthält keine Aufgaben und führt nichts aus. Listen per Drag-and-Drop hineinziehen.",
|
||||||
"sectionGeneral": "ALLGEMEIN",
|
"sectionGeneral": "ALLGEMEIN",
|
||||||
"name": "Name",
|
"name": "Name",
|
||||||
"workingDirectory": "Arbeitsverzeichnis",
|
"workingDirectory": "Arbeitsverzeichnis",
|
||||||
@@ -793,7 +799,7 @@
|
|||||||
"sessionSkillsTab": { "installed": "Installiert: {0}", "installFailed": "Installation fehlgeschlagen: {0}", "updated": "Aktualisiert.", "updateFailed": "Aktualisierung fehlgeschlagen: {0}", "removed": "Entfernt.", "removeFailed": "Entfernen fehlgeschlagen: {0}" },
|
"sessionSkillsTab": { "installed": "Installiert: {0}", "installFailed": "Installation fehlgeschlagen: {0}", "updated": "Aktualisiert.", "updateFailed": "Aktualisierung fehlgeschlagen: {0}", "removed": "Entfernt.", "removeFailed": "Entfernen fehlgeschlagen: {0}" },
|
||||||
"worktreesTab": { "workerOffline": "Worker offline.", "removed": "{0} Worktree(s) entfernt.", "blocked": "Zwangsentfernung nicht möglich: {0} Aufgabe(n) laufen noch. Brich sie zuerst ab.", "removedFrom": "{0} Worktree(s) von {1} Aufgabe(n) entfernt.", "cleanupFailed": "Aufräumen fehlgeschlagen: {0}", "resetFailed": "Zurücksetzen fehlgeschlagen: {0}" },
|
"worktreesTab": { "workerOffline": "Worker offline.", "removed": "{0} Worktree(s) entfernt.", "blocked": "Zwangsentfernung nicht möglich: {0} Aufgabe(n) laufen noch. Brich sie zuerst ab.", "removedFrom": "{0} Worktree(s) von {1} Aufgabe(n) entfernt.", "cleanupFailed": "Aufräumen fehlgeschlagen: {0}", "resetFailed": "Zurücksetzen fehlgeschlagen: {0}" },
|
||||||
"worktreesOverview": { "titleAll": "Worktrees", "titleList": "Worktrees — {0}", "listFallback": "Liste", "cleanupFailed": "Aufräumen fehlgeschlagen.", "cleanupFailedDetailed": "Aufräumen fehlgeschlagen: {0}", "removed": "{0} Worktree(s) entfernt.", "discardConfirm": "Worktree für \"{0}\" verwerfen? Nicht committete Arbeit geht verloren.", "discardFailed": "Worktree konnte nicht verworfen werden.", "keepFailed": "Worktree konnte nicht behalten werden.", "cannotForceRunning": "Eine laufende Aufgabe kann nicht zwangsweise entfernt werden.", "forceRemoveFailed": "Zwangsentfernung fehlgeschlagen.", "forceRemoveFailedDetailed": "Zwangsentfernung fehlgeschlagen: {0}", "batchDone": "{0} gemergt, {1} zu lösen.", "openInExplorerFailed": "Pfad konnte nicht geöffnet werden: {0}" },
|
"worktreesOverview": { "titleAll": "Worktrees", "titleList": "Worktrees — {0}", "listFallback": "Liste", "cleanupFailed": "Aufräumen fehlgeschlagen.", "cleanupFailedDetailed": "Aufräumen fehlgeschlagen: {0}", "removed": "{0} Worktree(s) entfernt.", "discardConfirm": "Worktree für \"{0}\" verwerfen? Nicht committete Arbeit geht verloren.", "discardFailed": "Worktree konnte nicht verworfen werden.", "keepFailed": "Worktree konnte nicht behalten werden.", "cannotForceRunning": "Eine laufende Aufgabe kann nicht zwangsweise entfernt werden.", "forceRemoveFailed": "Zwangsentfernung fehlgeschlagen.", "forceRemoveFailedDetailed": "Zwangsentfernung fehlgeschlagen: {0}", "batchDone": "{0} gemergt, {1} zu lösen.", "openInExplorerFailed": "Pfad konnte nicht geöffnet werden: {0}" },
|
||||||
"listSettings": { "untitled": "Unbenannt", "ticketProjectNone": "— keins —", "loadFailed": "Listeneinstellungen konnten nicht geladen werden: {0}", "ticketProjectsLoadFailed": "Ticket-Projekte konnten nicht geladen werden: {0}" },
|
"listSettings": { "untitled": "Unbenannt", "ticketProjectNone": "— keins —", "loadFailed": "Listeneinstellungen konnten nicht geladen werden: {0}", "ticketProjectsLoadFailed": "Ticket-Projekte konnten nicht geladen werden: {0}", "confirmDeleteFolder": "Ordner \"{0}\" löschen? Die Listen darin bleiben erhalten und wandern auf die oberste Ebene." },
|
||||||
"detailsIsland": { "verifyFailed": "Merge ist erfolgt, aber das Verifikationskommando der Liste ist fehlgeschlagen — die Aufgabe wurde nicht auf 'Erledigt' gesetzt.", "untrackedCollision": "Merge abgelehnt — er würde eine unversionierte Datei im Ziel-Arbeitsverzeichnis überschreiben.", "stopOffline": "Worker offline — Task kann nicht gestoppt werden.", "stopFailed": "Stoppen fehlgeschlagen: {0}", "enqueueFailed": "In die Warteschlange stellen fehlgeschlagen: {0}", "dequeueFailed": "Aus der Warteschlange entfernen fehlgeschlagen: {0}", "resetAndRetryFailed": "Zurücksetzen & erneut versuchen fehlgeschlagen: {0}", "toggleDoneFailed": "Erledigt-Status konnte nicht aktualisiert werden: {0}" },
|
"detailsIsland": { "verifyFailed": "Merge ist erfolgt, aber das Verifikationskommando der Liste ist fehlgeschlagen — die Aufgabe wurde nicht auf 'Erledigt' gesetzt.", "untrackedCollision": "Merge abgelehnt — er würde eine unversionierte Datei im Ziel-Arbeitsverzeichnis überschreiben.", "stopOffline": "Worker offline — Task kann nicht gestoppt werden.", "stopFailed": "Stoppen fehlgeschlagen: {0}", "enqueueFailed": "In die Warteschlange stellen fehlgeschlagen: {0}", "dequeueFailed": "Aus der Warteschlange entfernen fehlgeschlagen: {0}", "resetAndRetryFailed": "Zurücksetzen & erneut versuchen fehlgeschlagen: {0}", "toggleDoneFailed": "Erledigt-Status konnte nicht aktualisiert werden: {0}" },
|
||||||
"mergeSection": { "openWorktreeFailed": "Worktree konnte nicht geöffnet werden: {0}" },
|
"mergeSection": { "openWorktreeFailed": "Worktree konnte nicht geöffnet werden: {0}" },
|
||||||
"about": { "openPathFailed": "Pfad konnte nicht geöffnet werden: {0}" },
|
"about": { "openPathFailed": "Pfad konnte nicht geöffnet werden: {0}" },
|
||||||
|
|||||||
@@ -260,6 +260,8 @@
|
|||||||
"contextOpenFindings": "Open findings folder",
|
"contextOpenFindings": "Open findings folder",
|
||||||
"contextImportTickets": "Update from ticket system",
|
"contextImportTickets": "Update from ticket system",
|
||||||
"contextSettings": "Settings…",
|
"contextSettings": "Settings…",
|
||||||
|
"contextFolderSettings": "Folder settings…",
|
||||||
|
"contextRemoveFromFolder": "Move out of folder",
|
||||||
"newList": "New list",
|
"newList": "New list",
|
||||||
"addReposTip": "Add repos as lists",
|
"addReposTip": "Add repos as lists",
|
||||||
"noRepoBannerText": "No repo linked yet. ClaudeDo runs tasks in a git repo.",
|
"noRepoBannerText": "No repo linked yet. ClaudeDo runs tasks in a git repo.",
|
||||||
@@ -403,7 +405,11 @@
|
|||||||
},
|
},
|
||||||
"listSettings": {
|
"listSettings": {
|
||||||
"title": "LIST SETTINGS",
|
"title": "LIST SETTINGS",
|
||||||
|
"folderTitle": "FOLDER",
|
||||||
"deleteList": "Delete list",
|
"deleteList": "Delete list",
|
||||||
|
"deleteFolder": "Delete folder",
|
||||||
|
"createAsFolder": "Create a folder instead",
|
||||||
|
"createAsFolderHint": "A folder just groups lists in the sidebar — it holds no tasks and runs nothing. Drag lists onto it to fill it.",
|
||||||
"sectionGeneral": "GENERAL",
|
"sectionGeneral": "GENERAL",
|
||||||
"name": "Name",
|
"name": "Name",
|
||||||
"workingDirectory": "Working directory",
|
"workingDirectory": "Working directory",
|
||||||
@@ -793,7 +799,7 @@
|
|||||||
"sessionSkillsTab": { "installed": "Installed: {0}", "installFailed": "Install failed: {0}", "updated": "Updated.", "updateFailed": "Update failed: {0}", "removed": "Removed.", "removeFailed": "Remove failed: {0}" },
|
"sessionSkillsTab": { "installed": "Installed: {0}", "installFailed": "Install failed: {0}", "updated": "Updated.", "updateFailed": "Update failed: {0}", "removed": "Removed.", "removeFailed": "Remove failed: {0}" },
|
||||||
"worktreesTab": { "workerOffline": "Worker offline.", "removed": "Removed {0} worktree(s).", "blocked": "Cannot force-remove: {0} task(s) still running. Cancel them first.", "removedFrom": "Removed {0} worktree(s) from {1} task(s).", "cleanupFailed": "Cleanup failed: {0}", "resetFailed": "Reset failed: {0}" },
|
"worktreesTab": { "workerOffline": "Worker offline.", "removed": "Removed {0} worktree(s).", "blocked": "Cannot force-remove: {0} task(s) still running. Cancel them first.", "removedFrom": "Removed {0} worktree(s) from {1} task(s).", "cleanupFailed": "Cleanup failed: {0}", "resetFailed": "Reset failed: {0}" },
|
||||||
"worktreesOverview": { "titleAll": "Worktrees", "titleList": "Worktrees — {0}", "listFallback": "list", "cleanupFailed": "Cleanup failed.", "cleanupFailedDetailed": "Cleanup failed: {0}", "removed": "Removed {0} worktree(s).", "discardConfirm": "Discard the worktree for \"{0}\"? Uncommitted work will be lost.", "discardFailed": "Failed to discard worktree.", "keepFailed": "Failed to keep worktree.", "cannotForceRunning": "Cannot force-remove a running task.", "forceRemoveFailed": "Force remove failed.", "forceRemoveFailedDetailed": "Force remove failed: {0}", "batchDone": "Merged {0}, {1} need resolution.", "openInExplorerFailed": "Couldn't open path: {0}" },
|
"worktreesOverview": { "titleAll": "Worktrees", "titleList": "Worktrees — {0}", "listFallback": "list", "cleanupFailed": "Cleanup failed.", "cleanupFailedDetailed": "Cleanup failed: {0}", "removed": "Removed {0} worktree(s).", "discardConfirm": "Discard the worktree for \"{0}\"? Uncommitted work will be lost.", "discardFailed": "Failed to discard worktree.", "keepFailed": "Failed to keep worktree.", "cannotForceRunning": "Cannot force-remove a running task.", "forceRemoveFailed": "Force remove failed.", "forceRemoveFailedDetailed": "Force remove failed: {0}", "batchDone": "Merged {0}, {1} need resolution.", "openInExplorerFailed": "Couldn't open path: {0}" },
|
||||||
"listSettings": { "untitled": "Untitled", "ticketProjectNone": "— none —", "loadFailed": "Couldn't load list settings: {0}", "ticketProjectsLoadFailed": "Couldn't load ticket projects: {0}" },
|
"listSettings": { "untitled": "Untitled", "ticketProjectNone": "— none —", "loadFailed": "Couldn't load list settings: {0}", "ticketProjectsLoadFailed": "Couldn't load ticket projects: {0}", "confirmDeleteFolder": "Delete folder \"{0}\"? The lists inside it are kept and move back to the top level." },
|
||||||
"detailsIsland": { "verifyFailed": "Merge landed, but the list's verify command failed — the task was kept out of Done.", "untrackedCollision": "Merge refused — it would overwrite an untracked file in the target working directory.", "stopOffline": "Worker offline — can't stop the task.", "stopFailed": "Stop failed: {0}", "enqueueFailed": "Queue failed: {0}", "dequeueFailed": "Remove from queue failed: {0}", "resetAndRetryFailed": "Reset & retry failed: {0}", "toggleDoneFailed": "Couldn't update done state: {0}" },
|
"detailsIsland": { "verifyFailed": "Merge landed, but the list's verify command failed — the task was kept out of Done.", "untrackedCollision": "Merge refused — it would overwrite an untracked file in the target working directory.", "stopOffline": "Worker offline — can't stop the task.", "stopFailed": "Stop failed: {0}", "enqueueFailed": "Queue failed: {0}", "dequeueFailed": "Remove from queue failed: {0}", "resetAndRetryFailed": "Reset & retry failed: {0}", "toggleDoneFailed": "Couldn't update done state: {0}" },
|
||||||
"mergeSection": { "openWorktreeFailed": "Couldn't open worktree: {0}" },
|
"mergeSection": { "openWorktreeFailed": "Couldn't open worktree: {0}" },
|
||||||
"about": { "openPathFailed": "Couldn't open path: {0}" },
|
"about": { "openPathFailed": "Couldn't open path: {0}" },
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
using System.Collections.ObjectModel;
|
||||||
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
|
|
||||||
|
namespace ClaudeDo.Ui.ViewModels.Islands;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A sidebar folder row. Not a <see cref="ListNavItemViewModel"/> and not a
|
||||||
|
/// <see cref="ListKind"/> — a folder holds no tasks and is never selectable, so tapping it
|
||||||
|
/// toggles <see cref="IsCollapsed"/> instead of changing the selection.
|
||||||
|
/// </summary>
|
||||||
|
public sealed partial class ListFolderNavItemViewModel : ViewModelBase
|
||||||
|
{
|
||||||
|
public required string Id { get; init; }
|
||||||
|
[ObservableProperty] private string _name = "";
|
||||||
|
[ObservableProperty] private string _iconKey = "Folder";
|
||||||
|
[ObservableProperty] private bool _isCollapsed;
|
||||||
|
// Sum of the child lists' counts — a collapsed folder still shows what's inside it.
|
||||||
|
[ObservableProperty] private int _count;
|
||||||
|
// Set while a dragged list row hovers over this folder header (move-into-folder target).
|
||||||
|
[ObservableProperty] private bool _isDropTarget;
|
||||||
|
|
||||||
|
public ObservableCollection<ListNavItemViewModel> Lists { get; } = new();
|
||||||
|
}
|
||||||
@@ -26,4 +26,6 @@ public sealed partial class ListNavItemViewModel : ViewModelBase
|
|||||||
[ObservableProperty] private bool _isTaskDropTarget;
|
[ObservableProperty] private bool _isTaskDropTarget;
|
||||||
// Settable (not init) so a list-settings icon change refreshes the row in place.
|
// Settable (not init) so a list-settings icon change refreshes the row in place.
|
||||||
[ObservableProperty] private string? _iconKey;
|
[ObservableProperty] private string? _iconKey;
|
||||||
|
// The sidebar folder this row sits in (ListEntity.FolderId); null = top level.
|
||||||
|
[ObservableProperty] private string? _folderId;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,6 +26,9 @@ public sealed partial class ListsIslandViewModel : ViewModelBase, IDisposable
|
|||||||
/// island show the notes editor instead of rows.</summary>
|
/// island show the notes editor instead of rows.</summary>
|
||||||
public const string NotesListId = "smart:notes";
|
public const string NotesListId = "smart:notes";
|
||||||
|
|
||||||
|
/// <summary>Icon key a folder falls back to when it has none of its own.</summary>
|
||||||
|
public const string ListFolderIcon = "Folder";
|
||||||
|
|
||||||
private readonly IDbContextFactory<ClaudeDoDbContext> _dbFactory;
|
private readonly IDbContextFactory<ClaudeDoDbContext> _dbFactory;
|
||||||
private readonly IServiceProvider? _services;
|
private readonly IServiceProvider? _services;
|
||||||
private readonly IWorkerClient? _worker;
|
private readonly IWorkerClient? _worker;
|
||||||
@@ -202,7 +205,15 @@ public sealed partial class ListsIslandViewModel : ViewModelBase, IDisposable
|
|||||||
|
|
||||||
public ObservableCollection<ListNavItemViewModel> Items { get; } = new();
|
public ObservableCollection<ListNavItemViewModel> Items { get; } = new();
|
||||||
public ObservableCollection<ListNavItemViewModel> SmartLists { get; } = new();
|
public ObservableCollection<ListNavItemViewModel> SmartLists { get; } = new();
|
||||||
|
/// <summary>Every user list, flat, regardless of folder — the set the count refresh, the
|
||||||
|
/// row refresh and the no-repo banner work over.</summary>
|
||||||
public ObservableCollection<ListNavItemViewModel> UserLists { get; } = new();
|
public ObservableCollection<ListNavItemViewModel> UserLists { get; } = new();
|
||||||
|
/// <summary>Sidebar folders, rendered above the top-level lists.</summary>
|
||||||
|
public ObservableCollection<ListFolderNavItemViewModel> Folders { get; } = new();
|
||||||
|
/// <summary>The user lists that sit outside any folder. Together with
|
||||||
|
/// <see cref="Folders"/> this is what the sidebar binds to; <see cref="UserLists"/> stays
|
||||||
|
/// the flat superset.</summary>
|
||||||
|
public ObservableCollection<ListNavItemViewModel> RootLists { get; } = new();
|
||||||
|
|
||||||
[ObservableProperty] private string _searchText = "";
|
[ObservableProperty] private string _searchText = "";
|
||||||
[ObservableProperty] private ListNavItemViewModel? _selectedList;
|
[ObservableProperty] private ListNavItemViewModel? _selectedList;
|
||||||
@@ -279,6 +290,8 @@ public sealed partial class ListsIslandViewModel : ViewModelBase, IDisposable
|
|||||||
Items.Clear();
|
Items.Clear();
|
||||||
SmartLists.Clear();
|
SmartLists.Clear();
|
||||||
UserLists.Clear();
|
UserLists.Clear();
|
||||||
|
Folders.Clear();
|
||||||
|
RootLists.Clear();
|
||||||
|
|
||||||
var smart = new[]
|
var smart = new[]
|
||||||
{
|
{
|
||||||
@@ -299,6 +312,16 @@ public sealed partial class ListsIslandViewModel : ViewModelBase, IDisposable
|
|||||||
var ticketsConfigured = await IsTicketsConfiguredAsync();
|
var ticketsConfigured = await IsTicketsConfiguredAsync();
|
||||||
await using var ctx = await _dbFactory.CreateDbContextAsync(ct);
|
await using var ctx = await _dbFactory.CreateDbContextAsync(ct);
|
||||||
var lists = new ListRepository(ctx);
|
var lists = new ListRepository(ctx);
|
||||||
|
|
||||||
|
foreach (var f in await lists.GetFoldersAsync(ct))
|
||||||
|
Folders.Add(new ListFolderNavItemViewModel
|
||||||
|
{
|
||||||
|
Id = f.Id,
|
||||||
|
Name = f.Name,
|
||||||
|
IconKey = f.Icon ?? ListFolderIcon,
|
||||||
|
IsCollapsed = f.IsCollapsed,
|
||||||
|
});
|
||||||
|
|
||||||
var seedNames = new HashSet<string>(new[] { "My Day", "Important", "Planned" });
|
var seedNames = new HashSet<string>(new[] { "My Day", "Important", "Planned" });
|
||||||
foreach (var l in await lists.GetAllAsync(ct))
|
foreach (var l in await lists.GetAllAsync(ct))
|
||||||
{
|
{
|
||||||
@@ -315,9 +338,13 @@ public sealed partial class ListsIslandViewModel : ViewModelBase, IDisposable
|
|||||||
IsManual = l.IsManual,
|
IsManual = l.IsManual,
|
||||||
FindingsTracked = l.FindingsTracked,
|
FindingsTracked = l.FindingsTracked,
|
||||||
HasTicketProject = ticketsConfigured && cfg?.TicketProjectId > 0,
|
HasTicketProject = ticketsConfigured && cfg?.TicketProjectId > 0,
|
||||||
|
// A folder_id pointing at a folder that isn't there (shouldn't happen — the FK
|
||||||
|
// sets it null on delete) would otherwise hide the list entirely.
|
||||||
|
FolderId = Folders.Any(f => f.Id == l.FolderId) ? l.FolderId : null,
|
||||||
};
|
};
|
||||||
Items.Add(item);
|
Items.Add(item);
|
||||||
UserLists.Add(item);
|
UserLists.Add(item);
|
||||||
|
ContainerOf(item).Add(item);
|
||||||
}
|
}
|
||||||
ok = true;
|
ok = true;
|
||||||
}
|
}
|
||||||
@@ -365,6 +392,8 @@ public sealed partial class ListsIslandViewModel : ViewModelBase, IDisposable
|
|||||||
var filter = _filters.Resolve(item.Id);
|
var filter = _filters.Resolve(item.Id);
|
||||||
item.Count = filter is null ? 0 : all.Count(filter.ShouldCount);
|
item.Count = filter is null ? 0 : all.Count(filter.ShouldCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
RecomputeFolderCounts();
|
||||||
ok = true;
|
ok = true;
|
||||||
}
|
}
|
||||||
catch (OperationCanceledException) { throw; }
|
catch (OperationCanceledException) { throw; }
|
||||||
@@ -403,6 +432,7 @@ public sealed partial class ListsIslandViewModel : ViewModelBase, IDisposable
|
|||||||
};
|
};
|
||||||
Items.Add(item);
|
Items.Add(item);
|
||||||
UserLists.Add(item);
|
UserLists.Add(item);
|
||||||
|
RootLists.Add(item);
|
||||||
SelectedList = item;
|
SelectedList = item;
|
||||||
RecomputeHasNoLinkedRepo();
|
RecomputeHasNoLinkedRepo();
|
||||||
|
|
||||||
@@ -410,7 +440,8 @@ public sealed partial class ListsIslandViewModel : ViewModelBase, IDisposable
|
|||||||
{
|
{
|
||||||
var vm = _services.GetRequiredService<ListSettingsModalViewModel>();
|
var vm = _services.GetRequiredService<ListSettingsModalViewModel>();
|
||||||
vm.ErrorReported += msg => ErrorReported?.Invoke(msg);
|
vm.ErrorReported += msg => ErrorReported?.Invoke(msg);
|
||||||
vm.BeginLoad(entity.Id, entity.Name, entity.WorkingDir, entity.DefaultCommitType, entity.IsManual, entity.FindingsTracked, entity.Icon);
|
// canToggleFolder: only the create flow offers "make this a folder instead".
|
||||||
|
vm.BeginLoad(entity.Id, entity.Name, entity.WorkingDir, entity.DefaultCommitType, entity.IsManual, entity.FindingsTracked, entity.Icon, canToggleFolder: true);
|
||||||
await Dialogs.ShowListSettingsAsync(vm);
|
await Dialogs.ShowListSettingsAsync(vm);
|
||||||
if (!vm.Saved && !vm.Deleted)
|
if (!vm.Saved && !vm.Deleted)
|
||||||
{
|
{
|
||||||
@@ -432,6 +463,7 @@ public sealed partial class ListsIslandViewModel : ViewModelBase, IDisposable
|
|||||||
r.DropHintAbove = false;
|
r.DropHintAbove = false;
|
||||||
r.DropHintBelow = false;
|
r.DropHintBelow = false;
|
||||||
}
|
}
|
||||||
|
foreach (var f in Folders) f.IsDropTarget = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void SetDropHint(ListNavItemViewModel target, bool placeBelow)
|
public void SetDropHint(ListNavItemViewModel target, bool placeBelow)
|
||||||
@@ -442,28 +474,133 @@ public sealed partial class ListsIslandViewModel : ViewModelBase, IDisposable
|
|||||||
r.DropHintAbove = isTarget && !placeBelow;
|
r.DropHintAbove = isTarget && !placeBelow;
|
||||||
r.DropHintBelow = isTarget && placeBelow;
|
r.DropHintBelow = isTarget && placeBelow;
|
||||||
}
|
}
|
||||||
|
foreach (var f in Folders) f.IsDropTarget = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void SetFolderDropTarget(ListFolderNavItemViewModel? target)
|
||||||
|
{
|
||||||
|
foreach (var r in UserLists)
|
||||||
|
{
|
||||||
|
r.DropHintAbove = false;
|
||||||
|
r.DropHintBelow = false;
|
||||||
|
}
|
||||||
|
foreach (var f in Folders) f.IsDropTarget = ReferenceEquals(f, target);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>The collection a row is rendered in: its folder's <c>Lists</c>, or
|
||||||
|
/// <see cref="RootLists"/> when it has no folder.</summary>
|
||||||
|
public ObservableCollection<ListNavItemViewModel> ContainerOf(ListNavItemViewModel row) =>
|
||||||
|
Folders.FirstOrDefault(f => f.Id == row.FolderId)?.Lists ?? RootLists;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Drops <paramref name="source"/> at <paramref name="target"/>'s position. Since a drop
|
||||||
|
/// position implies a container, a drag onto a row in a different folder (or onto a top-level
|
||||||
|
/// row) also *moves* the list there — that's the only way to drag a list out of a folder.
|
||||||
|
/// </summary>
|
||||||
public async Task ReorderAsync(ListNavItemViewModel source, ListNavItemViewModel target, bool placeBelow)
|
public async Task ReorderAsync(ListNavItemViewModel source, ListNavItemViewModel target, bool placeBelow)
|
||||||
{
|
{
|
||||||
if (source.Kind != ListKind.User || target.Kind != ListKind.User) return;
|
if (source.Kind != ListKind.User || target.Kind != ListKind.User) return;
|
||||||
if (ReferenceEquals(source, target)) return;
|
if (ReferenceEquals(source, target)) return;
|
||||||
|
|
||||||
MoveWithinCollection(UserLists, source, target, placeBelow);
|
var folderChanged = source.FolderId != target.FolderId;
|
||||||
|
if (folderChanged)
|
||||||
|
{
|
||||||
|
ContainerOf(source).Remove(source);
|
||||||
|
source.FolderId = target.FolderId;
|
||||||
|
ContainerOf(source).Add(source);
|
||||||
|
}
|
||||||
|
|
||||||
|
MoveWithinCollection(ContainerOf(source), source, target, placeBelow);
|
||||||
|
await PersistOrderAsync(folderChanged ? source : null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Appends a list to a folder (or to the top level when <paramref name="folder"/> is
|
||||||
|
/// null) — the drop-onto-the-folder-header path, which also reaches collapsed and empty folders.</summary>
|
||||||
|
public async Task MoveToFolderAsync(ListNavItemViewModel source, ListFolderNavItemViewModel? folder)
|
||||||
|
{
|
||||||
|
if (source.Kind != ListKind.User) return;
|
||||||
|
if (source.FolderId == folder?.Id) return;
|
||||||
|
|
||||||
|
ContainerOf(source).Remove(source);
|
||||||
|
source.FolderId = folder?.Id;
|
||||||
|
ContainerOf(source).Add(source);
|
||||||
|
await PersistOrderAsync(source);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Writes the sidebar's visual order back as a single global <c>sort_order</c> sequence
|
||||||
|
/// (folders top to bottom, then the top-level lists) and, when a list changed folder, its new
|
||||||
|
/// <c>folder_id</c>.
|
||||||
|
/// </summary>
|
||||||
|
private async Task PersistOrderAsync(ListNavItemViewModel? movedRow)
|
||||||
|
{
|
||||||
|
var orderedIds = Folders.SelectMany(f => f.Lists)
|
||||||
|
.Concat(RootLists)
|
||||||
|
.Select(i => RawId(i.Id))
|
||||||
|
.ToList();
|
||||||
|
|
||||||
var orderedIds = UserLists.Select(i => i.Id["user:".Length..]).ToList();
|
|
||||||
var sw = System.Diagnostics.Stopwatch.StartNew();
|
var sw = System.Diagnostics.Stopwatch.StartNew();
|
||||||
var ok = false;
|
var ok = false;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await using var ctx = await _dbFactory.CreateDbContextAsync();
|
await using var ctx = await _dbFactory.CreateDbContextAsync();
|
||||||
var lists = new ListRepository(ctx);
|
var lists = new ListRepository(ctx);
|
||||||
|
if (movedRow is not null)
|
||||||
|
await lists.SetListFolderAsync(RawId(movedRow.Id), movedRow.FolderId);
|
||||||
await lists.ReorderAsync(orderedIds);
|
await lists.ReorderAsync(orderedIds);
|
||||||
ok = true;
|
ok = true;
|
||||||
}
|
}
|
||||||
finally { OperationTiming.Shared.Record("db", "ListsIsland.ReorderAsync", sw.Elapsed, ok); }
|
finally { OperationTiming.Shared.Record("db", "ListsIsland.ReorderAsync", sw.Elapsed, ok); }
|
||||||
|
|
||||||
|
RecomputeFolderCounts();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Context-menu counterpart to dragging a list out of a folder.</summary>
|
||||||
|
[RelayCommand]
|
||||||
|
private async Task RemoveFromFolderAsync(ListNavItemViewModel? row)
|
||||||
|
{
|
||||||
|
if (row is null) return;
|
||||||
|
await MoveToFolderAsync(row, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private async Task ToggleFolderAsync(ListFolderNavItemViewModel? folder)
|
||||||
|
{
|
||||||
|
if (folder is null) return;
|
||||||
|
folder.IsCollapsed = !folder.IsCollapsed;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await using var ctx = await _dbFactory.CreateDbContextAsync();
|
||||||
|
await new ListRepository(ctx).SetFolderCollapsedAsync(folder.Id, folder.IsCollapsed);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
// The fold itself already happened in the UI; only its persistence failed.
|
||||||
|
ErrorReported?.Invoke(ex.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private async Task OpenFolderSettingsAsync(ListFolderNavItemViewModel? folder)
|
||||||
|
{
|
||||||
|
if (folder is null || Dialogs is null || _services is null) return;
|
||||||
|
var vm = _services.GetRequiredService<ListSettingsModalViewModel>();
|
||||||
|
vm.ErrorReported += msg => ErrorReported?.Invoke(msg);
|
||||||
|
vm.BeginLoadFolder(folder.Id, folder.Name, folder.IconKey);
|
||||||
|
await Dialogs.ShowListSettingsAsync(vm);
|
||||||
|
// Rename, re-icon and delete all change the folder header (and delete releases its lists
|
||||||
|
// back to the top level) — one reload covers every outcome.
|
||||||
|
if (vm.Saved || vm.Deleted) await LoadAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RecomputeFolderCounts()
|
||||||
|
{
|
||||||
|
foreach (var f in Folders) f.Count = f.Lists.Sum(l => l.Count);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string RawId(string rowId) =>
|
||||||
|
rowId.StartsWith("user:", StringComparison.Ordinal) ? rowId["user:".Length..] : rowId;
|
||||||
|
|
||||||
private static void MoveWithinCollection(
|
private static void MoveWithinCollection(
|
||||||
ObservableCollection<ListNavItemViewModel> coll,
|
ObservableCollection<ListNavItemViewModel> coll,
|
||||||
ListNavItemViewModel source,
|
ListNavItemViewModel source,
|
||||||
@@ -570,6 +707,7 @@ public sealed partial class ListsIslandViewModel : ViewModelBase, IDisposable
|
|||||||
IsManual = entity.IsManual,
|
IsManual = entity.IsManual,
|
||||||
FindingsTracked = entity.FindingsTracked,
|
FindingsTracked = entity.FindingsTracked,
|
||||||
HasTicketProject = hasTicketProject,
|
HasTicketProject = hasTicketProject,
|
||||||
|
FolderId = Folders.Any(f => f.Id == entity.FolderId) ? entity.FolderId : null,
|
||||||
};
|
};
|
||||||
|
|
||||||
var seedNames = new HashSet<string>(new[] { "My Day", "Important", "Planned" });
|
var seedNames = new HashSet<string>(new[] { "My Day", "Important", "Planned" });
|
||||||
@@ -579,6 +717,15 @@ public sealed partial class ListsIslandViewModel : ViewModelBase, IDisposable
|
|||||||
|
|
||||||
UserLists.Insert(insertAt, item);
|
UserLists.Insert(insertAt, item);
|
||||||
Items.Insert(SmartLists.Count + insertAt, item);
|
Items.Insert(SmartLists.Count + insertAt, item);
|
||||||
|
|
||||||
|
// Same DB-order position, but counted within the row's own container.
|
||||||
|
var container = ContainerOf(item);
|
||||||
|
var containerIdx = ordered
|
||||||
|
.Where(l => l.FolderId == item.FolderId)
|
||||||
|
.Select(l => l.Id)
|
||||||
|
.ToList()
|
||||||
|
.IndexOf(entity.Id);
|
||||||
|
container.Insert(containerIdx < 0 || containerIdx > container.Count ? container.Count : containerIdx, item);
|
||||||
RecomputeHasNoLinkedRepo();
|
RecomputeHasNoLinkedRepo();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,7 +55,33 @@ public sealed partial class ListSettingsModalViewModel : ViewModelBase
|
|||||||
// Dropdown hidden entirely when no ticket-system base URL is configured (Settings → Files tab).
|
// Dropdown hidden entirely when no ticket-system base URL is configured (Settings → Files tab).
|
||||||
// Icon key into the UI icon library (IconKeyConverter -> "Icon.<key>" StreamGeometry).
|
// Icon key into the UI icon library (IconKeyConverter -> "Icon.<key>" StreamGeometry).
|
||||||
[ObservableProperty] private string _icon = DefaultIcon;
|
[ObservableProperty] private string _icon = DefaultIcon;
|
||||||
[ObservableProperty] private bool _ticketsAvailable;
|
[ObservableProperty]
|
||||||
|
[NotifyPropertyChangedFor(nameof(ShowTicketProject))]
|
||||||
|
private bool _ticketsAvailable;
|
||||||
|
|
||||||
|
/// <summary>The dropdown needs a configured ticket system *and* a list — folders link to nothing.</summary>
|
||||||
|
public bool ShowTicketProject => TicketsAvailable && !IsFolder;
|
||||||
|
|
||||||
|
// ---- Folder mode ----
|
||||||
|
// A folder is a sidebar grouping, not a list: it carries only a name and an icon, so every
|
||||||
|
// other field/section collapses. Reached two ways — the "create a folder instead" toggle in
|
||||||
|
// the create flow (FolderId still null), and editing an existing folder (FolderId set).
|
||||||
|
// There is no list <-> folder conversion: the toggle is offered on create only.
|
||||||
|
[ObservableProperty]
|
||||||
|
[NotifyPropertyChangedFor(nameof(HeaderTitle))]
|
||||||
|
[NotifyPropertyChangedFor(nameof(DeleteLabel))]
|
||||||
|
[NotifyPropertyChangedFor(nameof(ShowTicketProject))]
|
||||||
|
private bool _isFolder;
|
||||||
|
|
||||||
|
/// <summary>True only while creating — an existing list can't become a folder or back.</summary>
|
||||||
|
[ObservableProperty] private bool _canToggleFolder;
|
||||||
|
|
||||||
|
private string? _folderId;
|
||||||
|
|
||||||
|
// Resolved rather than a {loc:Tr} key because the text depends on IsFolder.
|
||||||
|
public string HeaderTitle => Loc.T(IsFolder ? "modals.listSettings.folderTitle" : "modals.listSettings.title");
|
||||||
|
|
||||||
|
public string DeleteLabel => Loc.T(IsFolder ? "modals.listSettings.deleteFolder" : "modals.listSettings.deleteList");
|
||||||
[ObservableProperty] private TicketProjectOption? _selectedTicketProject;
|
[ObservableProperty] private TicketProjectOption? _selectedTicketProject;
|
||||||
public ObservableCollection<TicketProjectOption> TicketProjects { get; } = new();
|
public ObservableCollection<TicketProjectOption> TicketProjects { get; } = new();
|
||||||
|
|
||||||
@@ -123,8 +149,25 @@ public sealed partial class ListSettingsModalViewModel : ViewModelBase
|
|||||||
string defaultCommitType,
|
string defaultCommitType,
|
||||||
bool isManual = false,
|
bool isManual = false,
|
||||||
bool findingsTracked = false,
|
bool findingsTracked = false,
|
||||||
string? icon = null)
|
string? icon = null,
|
||||||
=> _ = LoadAsync(listId, name, workingDir, defaultCommitType, isManual, findingsTracked, icon);
|
bool canToggleFolder = false)
|
||||||
|
{
|
||||||
|
CanToggleFolder = canToggleFolder;
|
||||||
|
_ = LoadAsync(listId, name, workingDir, defaultCommitType, isManual, findingsTracked, icon);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Opens the modal on an existing folder: name + icon only, no list config to load and
|
||||||
|
/// nothing to fetch from the worker, so this needs no async counterpart.
|
||||||
|
/// </summary>
|
||||||
|
public void BeginLoadFolder(string folderId, string name, string? icon)
|
||||||
|
{
|
||||||
|
_folderId = folderId;
|
||||||
|
IsFolder = true;
|
||||||
|
CanToggleFolder = false;
|
||||||
|
Name = name;
|
||||||
|
Icon = IconOptions.Contains(icon) ? icon! : DefaultIcon;
|
||||||
|
}
|
||||||
|
|
||||||
public async Task LoadAsync(
|
public async Task LoadAsync(
|
||||||
string listId,
|
string listId,
|
||||||
@@ -213,6 +256,12 @@ public sealed partial class ListSettingsModalViewModel : ViewModelBase
|
|||||||
[RelayCommand(CanExecute = nameof(CanSave))]
|
[RelayCommand(CanExecute = nameof(CanSave))]
|
||||||
private async Task SaveAsync()
|
private async Task SaveAsync()
|
||||||
{
|
{
|
||||||
|
if (IsFolder)
|
||||||
|
{
|
||||||
|
await SaveFolderAsync();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
await _worker.UpdateListAsync(new UpdateListDto(
|
await _worker.UpdateListAsync(new UpdateListDto(
|
||||||
ListId,
|
ListId,
|
||||||
string.IsNullOrWhiteSpace(Name) ? Loc.T("vm.listSettings.untitled") : Name,
|
string.IsNullOrWhiteSpace(Name) ? Loc.T("vm.listSettings.untitled") : Name,
|
||||||
@@ -239,9 +288,45 @@ public sealed partial class ListSettingsModalViewModel : ViewModelBase
|
|||||||
CloseAction?.Invoke();
|
CloseAction?.Invoke();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task SaveFolderAsync()
|
||||||
|
{
|
||||||
|
var name = string.IsNullOrWhiteSpace(Name) ? Loc.T("vm.listSettings.untitled") : Name;
|
||||||
|
await using var ctx = await _dbFactory.CreateDbContextAsync();
|
||||||
|
var lists = new ListRepository(ctx);
|
||||||
|
|
||||||
|
if (_folderId is null)
|
||||||
|
{
|
||||||
|
await lists.AddFolderAsync(new ListFolderEntity
|
||||||
|
{
|
||||||
|
Id = Guid.NewGuid().ToString("N"),
|
||||||
|
Name = name,
|
||||||
|
Icon = Icon,
|
||||||
|
CreatedAt = DateTime.UtcNow,
|
||||||
|
});
|
||||||
|
// The create flow pre-creates a list row for this modal to save against; a folder
|
||||||
|
// doesn't need it. Deleted=true (it genuinely is) makes the caller do a full reload,
|
||||||
|
// which is what picks the new folder up.
|
||||||
|
await lists.DeleteAsync(ListId);
|
||||||
|
Deleted = true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
await lists.RenameFolderAsync(_folderId, name, Icon);
|
||||||
|
}
|
||||||
|
|
||||||
|
Saved = true;
|
||||||
|
CloseAction?.Invoke();
|
||||||
|
}
|
||||||
|
|
||||||
[RelayCommand(CanExecute = nameof(CanDelete))]
|
[RelayCommand(CanExecute = nameof(CanDelete))]
|
||||||
private async Task DeleteAsync()
|
private async Task DeleteAsync()
|
||||||
{
|
{
|
||||||
|
if (IsFolder)
|
||||||
|
{
|
||||||
|
await DeleteFolderAsync();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
var displayName = string.IsNullOrWhiteSpace(Name) ? Loc.T("vm.listSettings.untitled") : Name;
|
var displayName = string.IsNullOrWhiteSpace(Name) ? Loc.T("vm.listSettings.untitled") : Name;
|
||||||
if (ConfirmAsync is not null)
|
if (ConfirmAsync is not null)
|
||||||
{
|
{
|
||||||
@@ -270,6 +355,34 @@ public sealed partial class ListSettingsModalViewModel : ViewModelBase
|
|||||||
CloseAction?.Invoke();
|
CloseAction?.Invoke();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task DeleteFolderAsync()
|
||||||
|
{
|
||||||
|
// Create mode with the folder toggle on: nothing has been written yet, so this is a cancel
|
||||||
|
// — and the caller's rollback drops the pre-created list row.
|
||||||
|
if (_folderId is null) { CloseAction?.Invoke(); return; }
|
||||||
|
|
||||||
|
var displayName = string.IsNullOrWhiteSpace(Name) ? Loc.T("vm.listSettings.untitled") : Name;
|
||||||
|
if (ConfirmAsync is not null)
|
||||||
|
{
|
||||||
|
var ok = await ConfirmAsync(Loc.T("vm.listSettings.confirmDeleteFolder", displayName));
|
||||||
|
if (!ok) return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await using var ctx = await _dbFactory.CreateDbContextAsync();
|
||||||
|
await new ListRepository(ctx).DeleteFolderAsync(_folderId);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
ErrorReported?.Invoke(ex.Message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Deleted = true;
|
||||||
|
CloseAction?.Invoke();
|
||||||
|
}
|
||||||
|
|
||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
private void Cancel() => CloseAction?.Invoke();
|
private void Cancel() => CloseAction?.Invoke();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,91 @@
|
|||||||
xmlns:loc="using:ClaudeDo.Ui.Localization"
|
xmlns:loc="using:ClaudeDo.Ui.Localization"
|
||||||
x:Class="ClaudeDo.Ui.Views.Islands.ListsIslandView"
|
x:Class="ClaudeDo.Ui.Views.Islands.ListsIslandView"
|
||||||
x:DataType="vm:ListsIslandViewModel">
|
x:DataType="vm:ListsIslandViewModel">
|
||||||
|
<UserControl.Resources>
|
||||||
|
<!-- One user-list row. Shared by the top-level list and by each folder's children, so the
|
||||||
|
row (context menu, drag handlers, drop indicators) exists once. -->
|
||||||
|
<DataTemplate x:Key="UserListRow" DataType="vm:ListNavItemViewModel">
|
||||||
|
<Grid RowDefinitions="Auto,Auto,Auto">
|
||||||
|
|
||||||
|
<!-- Above-row drop indicator -->
|
||||||
|
<Border Grid.Row="0" Height="2" VerticalAlignment="Center" Margin="4,0"
|
||||||
|
Background="{DynamicResource MossBrush}" CornerRadius="1"
|
||||||
|
IsVisible="{Binding DropHintAbove}"/>
|
||||||
|
|
||||||
|
<Border Grid.Row="1" Classes="list-item" Classes.active="{Binding IsActive}"
|
||||||
|
Classes.drop-target="{Binding IsTaskDropTarget}"
|
||||||
|
Tapped="OnItemTapped"
|
||||||
|
DragDrop.AllowDrop="True"
|
||||||
|
DragDrop.DragOver="OnListDragOver"
|
||||||
|
DragDrop.Drop="OnListDrop">
|
||||||
|
<Border.ContextMenu>
|
||||||
|
<ContextMenu>
|
||||||
|
<MenuItem Header="{loc:Tr lists.contextWorktrees}"
|
||||||
|
Command="{Binding $parent[UserControl].((vm:ListsIslandViewModel)DataContext).OpenWorktreesOverviewCommand}"
|
||||||
|
CommandParameter="{Binding}"/>
|
||||||
|
<Separator IsVisible="{Binding WorkingDir, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
|
||||||
|
<MenuItem Header="{loc:Tr lists.contextOpenExplorer}"
|
||||||
|
IsVisible="{Binding WorkingDir, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"
|
||||||
|
Command="{Binding $parent[UserControl].((vm:ListsIslandViewModel)DataContext).OpenInExplorerCommand}"
|
||||||
|
CommandParameter="{Binding}"/>
|
||||||
|
<MenuItem Header="{loc:Tr lists.contextOpenFindings}"
|
||||||
|
IsVisible="{Binding WorkingDir, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"
|
||||||
|
Command="{Binding $parent[UserControl].((vm:ListsIslandViewModel)DataContext).OpenFindingsCommand}"
|
||||||
|
CommandParameter="{Binding}"/>
|
||||||
|
<MenuItem Header="{loc:Tr lists.contextOpenTerminal}"
|
||||||
|
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.contextRemoveFromFolder}"
|
||||||
|
IsVisible="{Binding FolderId, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"
|
||||||
|
Command="{Binding $parent[UserControl].((vm:ListsIslandViewModel)DataContext).RemoveFromFolderCommand}"
|
||||||
|
CommandParameter="{Binding}"/>
|
||||||
|
<MenuItem Header="{loc:Tr lists.contextSettings}"
|
||||||
|
Command="{Binding $parent[UserControl].((vm:ListsIslandViewModel)DataContext).OpenListSettingsCommand}"
|
||||||
|
CommandParameter="{Binding}"/>
|
||||||
|
</ContextMenu>
|
||||||
|
</Border.ContextMenu>
|
||||||
|
<Grid ColumnDefinitions="20,*,Auto">
|
||||||
|
<!-- Left accent bar for active state -->
|
||||||
|
<Border Grid.Column="0" Grid.ColumnSpan="3"
|
||||||
|
Background="Transparent"
|
||||||
|
CornerRadius="8" IsHitTestVisible="False"
|
||||||
|
IsVisible="{Binding IsActive}">
|
||||||
|
<Border Width="2" Height="16"
|
||||||
|
Background="{DynamicResource AccentBrush}"
|
||||||
|
CornerRadius="1"
|
||||||
|
HorizontalAlignment="Left" VerticalAlignment="Center"
|
||||||
|
Margin="-8,0,0,0"/>
|
||||||
|
</Border>
|
||||||
|
<!-- Icon (list-settings → Icon tab; "Folder" when unset) -->
|
||||||
|
<PathIcon Grid.Column="0" Classes="list-icon"
|
||||||
|
Width="14" Height="14"
|
||||||
|
Data="{Binding IconKey, Converter={StaticResource IconKey}}"
|
||||||
|
HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||||
|
<!-- Name -->
|
||||||
|
<TextBlock Grid.Column="1" Classes="list-label"
|
||||||
|
Text="{Binding Name}"
|
||||||
|
VerticalAlignment="Center" Margin="8,0"
|
||||||
|
Foreground="{DynamicResource TextDimBrush}" FontSize="{StaticResource FontSizeBody}"/>
|
||||||
|
<!-- Count -->
|
||||||
|
<TextBlock Grid.Column="2" Classes="list-count"
|
||||||
|
Text="{Binding Count}"/>
|
||||||
|
</Grid>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<!-- Below-row drop indicator (last item only) -->
|
||||||
|
<Border Grid.Row="2" Height="2" VerticalAlignment="Center" Margin="4,0"
|
||||||
|
Background="{DynamicResource MossBrush}" CornerRadius="1"
|
||||||
|
IsVisible="{Binding DropHintBelow}"/>
|
||||||
|
</Grid>
|
||||||
|
</DataTemplate>
|
||||||
|
</UserControl.Resources>
|
||||||
|
|
||||||
<DockPanel LastChildFill="True">
|
<DockPanel LastChildFill="True">
|
||||||
|
|
||||||
<!-- ── Header ── -->
|
<!-- ── Header ── -->
|
||||||
@@ -133,87 +218,61 @@
|
|||||||
<PathIcon Data="{StaticResource Icon.Folder}" Width="14" Height="14"/>
|
<PathIcon Data="{StaticResource Icon.Folder}" Width="14" Height="14"/>
|
||||||
</Button>
|
</Button>
|
||||||
</Grid>
|
</Grid>
|
||||||
<ItemsControl ItemsSource="{Binding UserLists}">
|
<!-- Folders first, each with its own (collapsible) child lists, then the top-level lists.
|
||||||
|
Folders are never selectable — tapping the header folds it. -->
|
||||||
|
<ItemsControl ItemsSource="{Binding Folders}">
|
||||||
<ItemsControl.ItemTemplate>
|
<ItemsControl.ItemTemplate>
|
||||||
<DataTemplate DataType="vm:ListNavItemViewModel">
|
<DataTemplate DataType="vm:ListFolderNavItemViewModel">
|
||||||
<Grid RowDefinitions="Auto,Auto,Auto">
|
<StackPanel>
|
||||||
|
<Border Classes="list-item"
|
||||||
|
Classes.drop-target="{Binding IsDropTarget}"
|
||||||
|
Tapped="OnFolderTapped"
|
||||||
|
DragDrop.AllowDrop="True"
|
||||||
|
DragDrop.DragOver="OnFolderDragOver"
|
||||||
|
DragDrop.Drop="OnFolderDrop">
|
||||||
|
<Border.ContextMenu>
|
||||||
|
<ContextMenu>
|
||||||
|
<MenuItem Header="{loc:Tr lists.contextFolderSettings}"
|
||||||
|
Command="{Binding $parent[UserControl].((vm:ListsIslandViewModel)DataContext).OpenFolderSettingsCommand}"
|
||||||
|
CommandParameter="{Binding}"/>
|
||||||
|
</ContextMenu>
|
||||||
|
</Border.ContextMenu>
|
||||||
|
<Grid ColumnDefinitions="12,20,*,Auto">
|
||||||
|
<PathIcon Grid.Column="0" Classes="list-icon"
|
||||||
|
Width="9" Height="9"
|
||||||
|
Data="{StaticResource Icon.ChevronRight}"
|
||||||
|
IsVisible="{Binding IsCollapsed}"
|
||||||
|
HorizontalAlignment="Left" VerticalAlignment="Center"/>
|
||||||
|
<PathIcon Grid.Column="0" Classes="list-icon"
|
||||||
|
Width="9" Height="9"
|
||||||
|
Data="{StaticResource Icon.ChevronDown}"
|
||||||
|
IsVisible="{Binding !IsCollapsed}"
|
||||||
|
HorizontalAlignment="Left" VerticalAlignment="Center"/>
|
||||||
|
<PathIcon Grid.Column="1" Classes="list-icon"
|
||||||
|
Width="14" Height="14"
|
||||||
|
Data="{Binding IconKey, Converter={StaticResource IconKey}}"
|
||||||
|
HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||||
|
<TextBlock Grid.Column="2" Classes="list-label"
|
||||||
|
Text="{Binding Name}"
|
||||||
|
VerticalAlignment="Center" Margin="8,0"
|
||||||
|
Foreground="{DynamicResource TextDimBrush}" FontSize="{StaticResource FontSizeBody}"/>
|
||||||
|
<TextBlock Grid.Column="3" Classes="list-count"
|
||||||
|
Text="{Binding Count}"/>
|
||||||
|
</Grid>
|
||||||
|
</Border>
|
||||||
|
|
||||||
<!-- Above-row drop indicator -->
|
<ItemsControl ItemsSource="{Binding Lists}"
|
||||||
<Border Grid.Row="0" Height="2" VerticalAlignment="Center" Margin="4,0"
|
IsVisible="{Binding !IsCollapsed}"
|
||||||
Background="{DynamicResource MossBrush}" CornerRadius="1"
|
Margin="14,0,0,0"
|
||||||
IsVisible="{Binding DropHintAbove}"/>
|
ItemTemplate="{StaticResource UserListRow}"/>
|
||||||
|
</StackPanel>
|
||||||
<Border Grid.Row="1" Classes="list-item" Classes.active="{Binding IsActive}"
|
|
||||||
Classes.drop-target="{Binding IsTaskDropTarget}"
|
|
||||||
Tapped="OnItemTapped"
|
|
||||||
DragDrop.AllowDrop="True"
|
|
||||||
DragDrop.DragOver="OnListDragOver"
|
|
||||||
DragDrop.Drop="OnListDrop">
|
|
||||||
<Border.ContextMenu>
|
|
||||||
<ContextMenu>
|
|
||||||
<MenuItem Header="{loc:Tr lists.contextWorktrees}"
|
|
||||||
Command="{Binding $parent[UserControl].((vm:ListsIslandViewModel)DataContext).OpenWorktreesOverviewCommand}"
|
|
||||||
CommandParameter="{Binding}"/>
|
|
||||||
<Separator IsVisible="{Binding WorkingDir, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
|
|
||||||
<MenuItem Header="{loc:Tr lists.contextOpenExplorer}"
|
|
||||||
IsVisible="{Binding WorkingDir, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"
|
|
||||||
Command="{Binding $parent[UserControl].((vm:ListsIslandViewModel)DataContext).OpenInExplorerCommand}"
|
|
||||||
CommandParameter="{Binding}"/>
|
|
||||||
<MenuItem Header="{loc:Tr lists.contextOpenFindings}"
|
|
||||||
IsVisible="{Binding WorkingDir, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"
|
|
||||||
Command="{Binding $parent[UserControl].((vm:ListsIslandViewModel)DataContext).OpenFindingsCommand}"
|
|
||||||
CommandParameter="{Binding}"/>
|
|
||||||
<MenuItem Header="{loc:Tr lists.contextOpenTerminal}"
|
|
||||||
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}"
|
|
||||||
CommandParameter="{Binding}"/>
|
|
||||||
</ContextMenu>
|
|
||||||
</Border.ContextMenu>
|
|
||||||
<Grid ColumnDefinitions="20,*,Auto">
|
|
||||||
<!-- Left accent bar for active state -->
|
|
||||||
<Border Grid.Column="0" Grid.ColumnSpan="3"
|
|
||||||
Background="Transparent"
|
|
||||||
CornerRadius="8" IsHitTestVisible="False"
|
|
||||||
IsVisible="{Binding IsActive}">
|
|
||||||
<Border Width="2" Height="16"
|
|
||||||
Background="{DynamicResource AccentBrush}"
|
|
||||||
CornerRadius="1"
|
|
||||||
HorizontalAlignment="Left" VerticalAlignment="Center"
|
|
||||||
Margin="-8,0,0,0"/>
|
|
||||||
</Border>
|
|
||||||
<!-- Icon (list-settings → Icon tab; "Folder" when unset) -->
|
|
||||||
<PathIcon Grid.Column="0" Classes="list-icon"
|
|
||||||
Width="14" Height="14"
|
|
||||||
Data="{Binding IconKey, Converter={StaticResource IconKey}}"
|
|
||||||
HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
|
||||||
<!-- Name -->
|
|
||||||
<TextBlock Grid.Column="1" Classes="list-label"
|
|
||||||
Text="{Binding Name}"
|
|
||||||
VerticalAlignment="Center" Margin="8,0"
|
|
||||||
Foreground="{DynamicResource TextDimBrush}" FontSize="{StaticResource FontSizeBody}"/>
|
|
||||||
<!-- Count -->
|
|
||||||
<TextBlock Grid.Column="2" Classes="list-count"
|
|
||||||
Text="{Binding Count}"/>
|
|
||||||
</Grid>
|
|
||||||
</Border>
|
|
||||||
|
|
||||||
<!-- Below-row drop indicator (last item only) -->
|
|
||||||
<Border Grid.Row="2" Height="2" VerticalAlignment="Center" Margin="4,0"
|
|
||||||
Background="{DynamicResource MossBrush}" CornerRadius="1"
|
|
||||||
IsVisible="{Binding DropHintBelow}"/>
|
|
||||||
</Grid>
|
|
||||||
</DataTemplate>
|
</DataTemplate>
|
||||||
</ItemsControl.ItemTemplate>
|
</ItemsControl.ItemTemplate>
|
||||||
</ItemsControl>
|
</ItemsControl>
|
||||||
|
|
||||||
|
<ItemsControl ItemsSource="{Binding RootLists}"
|
||||||
|
ItemTemplate="{StaticResource UserListRow}"/>
|
||||||
|
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</ScrollViewer>
|
</ScrollViewer>
|
||||||
</DockPanel>
|
</DockPanel>
|
||||||
|
|||||||
@@ -69,6 +69,60 @@ public partial class ListsIslandView : UserControl
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void OnFolderTapped(object? sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (sender is Border { DataContext: ListFolderNavItemViewModel folder }
|
||||||
|
&& DataContext is ListsIslandViewModel vm)
|
||||||
|
vm.ToggleFolderCommand.Execute(folder);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dropping onto the folder *header* appends the list to that folder — the only route into a
|
||||||
|
// collapsed or empty folder, where there's no child row to drop next to.
|
||||||
|
private void OnFolderDragOver(object? sender, DragEventArgs e)
|
||||||
|
{
|
||||||
|
if (DataContext is not ListsIslandViewModel vm) { e.DragEffects = DragDropEffects.None; return; }
|
||||||
|
if ((!e.DataTransfer?.Contains(ListRowFormat) ?? true)
|
||||||
|
|| sender is not Border { DataContext: ListFolderNavItemViewModel folder })
|
||||||
|
{
|
||||||
|
e.DragEffects = DragDropEffects.None;
|
||||||
|
vm.ClearDropHints();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var sourceId = e.DataTransfer?.TryGetValue(ListRowFormat);
|
||||||
|
var source = sourceId is null ? null : vm.UserLists.FirstOrDefault(r => r.Id == sourceId);
|
||||||
|
if (source is null || source.FolderId == folder.Id)
|
||||||
|
{
|
||||||
|
e.DragEffects = DragDropEffects.None;
|
||||||
|
vm.ClearDropHints();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
vm.SetFolderDropTarget(folder);
|
||||||
|
e.DragEffects = DragDropEffects.Move;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void OnFolderDrop(object? sender, DragEventArgs e)
|
||||||
|
{
|
||||||
|
if (DataContext is not ListsIslandViewModel vm) return;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (sender is not Border { DataContext: ListFolderNavItemViewModel folder }) return;
|
||||||
|
|
||||||
|
var sourceId = e.DataTransfer?.TryGetValue(ListRowFormat);
|
||||||
|
var source = sourceId is null ? null : vm.UserLists.FirstOrDefault(r => r.Id == sourceId);
|
||||||
|
if (source is null) return;
|
||||||
|
|
||||||
|
vm.ClearDropHints();
|
||||||
|
await vm.MoveToFolderAsync(source, folder);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
vm.ClearDropHints();
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void OnListDragOver(object? sender, DragEventArgs e)
|
private void OnListDragOver(object? sender, DragEventArgs e)
|
||||||
{
|
{
|
||||||
if (DataContext is not ListsIslandViewModel vm) { e.DragEffects = DragDropEffects.None; return; }
|
if (DataContext is not ListsIslandViewModel vm) { e.DragEffects = DragDropEffects.None; return; }
|
||||||
@@ -150,11 +204,14 @@ public partial class ListsIslandView : UserControl
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Next row *within the same container* — the row below the last list of a folder belongs to
|
||||||
|
// another folder (or to the top level), so it is not a "drop above" candidate here.
|
||||||
private static ListNavItemViewModel? FindNextUserList(ListsIslandViewModel vm, ListNavItemViewModel row)
|
private static ListNavItemViewModel? FindNextUserList(ListsIslandViewModel vm, ListNavItemViewModel row)
|
||||||
{
|
{
|
||||||
var idx = vm.UserLists.IndexOf(row);
|
var container = vm.ContainerOf(row);
|
||||||
|
var idx = container.IndexOf(row);
|
||||||
if (idx < 0) return null;
|
if (idx < 0) return null;
|
||||||
return idx + 1 < vm.UserLists.Count ? vm.UserLists[idx + 1] : null;
|
return idx + 1 < container.Count ? container[idx + 1] : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,10 +20,10 @@
|
|||||||
<KeyBinding Gesture="Enter" Command="{Binding SaveCommand}"/>
|
<KeyBinding Gesture="Enter" Command="{Binding SaveCommand}"/>
|
||||||
</Window.KeyBindings>
|
</Window.KeyBindings>
|
||||||
|
|
||||||
<ctl:ModalShell Title="{loc:Tr modals.listSettings.title}" CloseCommand="{Binding CancelCommand}">
|
<ctl:ModalShell Title="{Binding HeaderTitle}" CloseCommand="{Binding CancelCommand}">
|
||||||
<ctl:ModalShell.Footer>
|
<ctl:ModalShell.Footer>
|
||||||
<Grid ColumnDefinitions="Auto,*,Auto" VerticalAlignment="Center">
|
<Grid ColumnDefinitions="Auto,*,Auto" VerticalAlignment="Center">
|
||||||
<Button Grid.Column="0" Content="{loc:Tr modals.listSettings.deleteList}" Classes="danger"
|
<Button Grid.Column="0" Content="{Binding DeleteLabel}" Classes="danger"
|
||||||
Command="{Binding DeleteCommand}" MinWidth="90"/>
|
Command="{Binding DeleteCommand}" MinWidth="90"/>
|
||||||
<StackPanel Grid.Column="2" Orientation="Horizontal" Spacing="8">
|
<StackPanel Grid.Column="2" Orientation="Horizontal" Spacing="8">
|
||||||
<Button Classes="btn" Content="{loc:Tr settings.cancel}" Command="{Binding CancelCommand}" MinWidth="90"/>
|
<Button Classes="btn" Content="{loc:Tr settings.cancel}" Command="{Binding CancelCommand}" MinWidth="90"/>
|
||||||
@@ -40,8 +40,12 @@
|
|||||||
VerticalAlignment="Top">
|
VerticalAlignment="Top">
|
||||||
<ListBoxItem Content="{loc:Tr modals.listSettings.sectionGeneral}"/>
|
<ListBoxItem Content="{loc:Tr modals.listSettings.sectionGeneral}"/>
|
||||||
<ListBoxItem Content="{loc:Tr modals.listSettings.sectionIcon}"/>
|
<ListBoxItem Content="{loc:Tr modals.listSettings.sectionIcon}"/>
|
||||||
<ListBoxItem Content="{loc:Tr modals.listSettings.sectionAgent}"/>
|
<!-- A folder runs nothing, so it has neither agent config nor a verify command. The
|
||||||
<ListBoxItem Content="{loc:Tr modals.listSettings.sectionVerify}"/>
|
TabItems stay in place (index-driven host) and just become unreachable. -->
|
||||||
|
<ListBoxItem Content="{loc:Tr modals.listSettings.sectionAgent}"
|
||||||
|
IsVisible="{Binding !IsFolder}"/>
|
||||||
|
<ListBoxItem Content="{loc:Tr modals.listSettings.sectionVerify}"
|
||||||
|
IsVisible="{Binding !IsFolder}"/>
|
||||||
</ListBox>
|
</ListBox>
|
||||||
|
|
||||||
<Border Grid.Column="1" Background="{DynamicResource LineBrush}" Width="1" Margin="0,8"/>
|
<Border Grid.Column="1" Background="{DynamicResource LineBrush}" Width="1" Margin="0,8"/>
|
||||||
@@ -69,12 +73,22 @@
|
|||||||
<TabItem Header="{loc:Tr modals.listSettings.sectionGeneral}">
|
<TabItem Header="{loc:Tr modals.listSettings.sectionGeneral}">
|
||||||
<ScrollViewer>
|
<ScrollViewer>
|
||||||
<StackPanel Spacing="12" Margin="0,8,0,0">
|
<StackPanel Spacing="12" Margin="0,8,0,0">
|
||||||
|
<!-- Create flow only: turns this into a sidebar folder (name + icon, nothing else).
|
||||||
|
Deliberately not a second sidebar button — the "+" and the repo-import button
|
||||||
|
already sit there. -->
|
||||||
|
<StackPanel Spacing="4" IsVisible="{Binding CanToggleFolder}">
|
||||||
|
<CheckBox IsChecked="{Binding IsFolder, Mode=TwoWay}"
|
||||||
|
Content="{loc:Tr modals.listSettings.createAsFolder}"/>
|
||||||
|
<TextBlock Text="{loc:Tr modals.listSettings.createAsFolderHint}"
|
||||||
|
Opacity="0.6" FontSize="12" TextWrapping="Wrap"/>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
<StackPanel Spacing="4">
|
<StackPanel Spacing="4">
|
||||||
<TextBlock Classes="field-label" Text="{loc:Tr modals.listSettings.name}"/>
|
<TextBlock Classes="field-label" Text="{loc:Tr modals.listSettings.name}"/>
|
||||||
<TextBox Text="{Binding Name, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
|
<TextBox Text="{Binding Name, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
<StackPanel Spacing="4">
|
<StackPanel Spacing="4" IsVisible="{Binding !IsFolder}">
|
||||||
<TextBlock Classes="field-label" Text="{loc:Tr modals.listSettings.workingDirectory}"/>
|
<TextBlock Classes="field-label" Text="{loc:Tr modals.listSettings.workingDirectory}"/>
|
||||||
<Grid ColumnDefinitions="*,Auto">
|
<Grid ColumnDefinitions="*,Auto">
|
||||||
<TextBox Grid.Column="0" Text="{Binding WorkingDir}" PlaceholderText="{loc:Tr modals.listSettings.workingDirectoryPlaceholder}" />
|
<TextBox Grid.Column="0" Text="{Binding WorkingDir}" PlaceholderText="{loc:Tr modals.listSettings.workingDirectoryPlaceholder}" />
|
||||||
@@ -82,35 +96,35 @@
|
|||||||
</Grid>
|
</Grid>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
<StackPanel Spacing="4">
|
<StackPanel Spacing="4" IsVisible="{Binding !IsFolder}">
|
||||||
<TextBlock Classes="field-label" Text="{loc:Tr modals.listSettings.defaultCommitType}"/>
|
<TextBlock Classes="field-label" Text="{loc:Tr modals.listSettings.defaultCommitType}"/>
|
||||||
<ComboBox ItemsSource="{Binding CommitTypeOptions}"
|
<ComboBox ItemsSource="{Binding CommitTypeOptions}"
|
||||||
SelectedItem="{Binding DefaultCommitType, Mode=TwoWay}"
|
SelectedItem="{Binding DefaultCommitType, Mode=TwoWay}"
|
||||||
HorizontalAlignment="Left" MinWidth="160" />
|
HorizontalAlignment="Left" MinWidth="160" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
<StackPanel Spacing="4">
|
<StackPanel Spacing="4" IsVisible="{Binding !IsFolder}">
|
||||||
<CheckBox IsChecked="{Binding IsManual, Mode=TwoWay}"
|
<CheckBox IsChecked="{Binding IsManual, Mode=TwoWay}"
|
||||||
Content="{loc:Tr modals.listSettings.manualList}"/>
|
Content="{loc:Tr modals.listSettings.manualList}"/>
|
||||||
<TextBlock Text="{loc:Tr modals.listSettings.manualListHint}"
|
<TextBlock Text="{loc:Tr modals.listSettings.manualListHint}"
|
||||||
Opacity="0.6" FontSize="12" TextWrapping="Wrap"/>
|
Opacity="0.6" FontSize="12" TextWrapping="Wrap"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
<StackPanel Spacing="4">
|
<StackPanel Spacing="4" IsVisible="{Binding !IsFolder}">
|
||||||
<CheckBox IsChecked="{Binding FindingsTracked, Mode=TwoWay}"
|
<CheckBox IsChecked="{Binding FindingsTracked, Mode=TwoWay}"
|
||||||
Content="{loc:Tr modals.listSettings.findingsTracked}"/>
|
Content="{loc:Tr modals.listSettings.findingsTracked}"/>
|
||||||
<TextBlock Text="{loc:Tr modals.listSettings.findingsTrackedHint}"
|
<TextBlock Text="{loc:Tr modals.listSettings.findingsTrackedHint}"
|
||||||
Opacity="0.6" FontSize="12" TextWrapping="Wrap"/>
|
Opacity="0.6" FontSize="12" TextWrapping="Wrap"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
<StackPanel Spacing="4">
|
<StackPanel Spacing="4" IsVisible="{Binding !IsFolder}">
|
||||||
<CheckBox IsChecked="{Binding PowerUseAllowed, Mode=TwoWay}"
|
<CheckBox IsChecked="{Binding PowerUseAllowed, Mode=TwoWay}"
|
||||||
Content="{loc:Tr modals.listSettings.powerUseAllowed}"/>
|
Content="{loc:Tr modals.listSettings.powerUseAllowed}"/>
|
||||||
<TextBlock Text="{loc:Tr modals.listSettings.powerUseAllowedHint}"
|
<TextBlock Text="{loc:Tr modals.listSettings.powerUseAllowedHint}"
|
||||||
Opacity="0.6" FontSize="12" TextWrapping="Wrap"/>
|
Opacity="0.6" FontSize="12" TextWrapping="Wrap"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
<StackPanel Spacing="4" IsVisible="{Binding TicketsAvailable}">
|
<StackPanel Spacing="4" IsVisible="{Binding ShowTicketProject}">
|
||||||
<TextBlock Classes="field-label" Text="{loc:Tr modals.listSettings.ticketProject}"/>
|
<TextBlock Classes="field-label" Text="{loc:Tr modals.listSettings.ticketProject}"/>
|
||||||
<ComboBox ItemsSource="{Binding TicketProjects}"
|
<ComboBox ItemsSource="{Binding TicketProjects}"
|
||||||
SelectedItem="{Binding SelectedTicketProject, Mode=TwoWay}"
|
SelectedItem="{Binding SelectedTicketProject, Mode=TwoWay}"
|
||||||
|
|||||||
Reference in New Issue
Block a user