Allow deleting projects from the project picker with x/X.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+153
-27
@@ -57,32 +57,33 @@ type Model struct {
|
||||
projects project.Store
|
||||
exec command.Executor
|
||||
|
||||
mode Mode
|
||||
tickets views.TicketList
|
||||
projectList list.Model
|
||||
statusMenu views.StatusMenu
|
||||
boardMenu views.BoardMenu
|
||||
boardFilter ticket.BoardFilter
|
||||
input textinput.Model
|
||||
report views.ReportView
|
||||
reportGroupBy ticket.GroupBy
|
||||
groupByMenu views.GroupByMenu
|
||||
timeEntries views.TimeEntriesView
|
||||
timeEntriesTicket string // ticket ID for ModeTimeEntries
|
||||
editingTimeEntry ticket.TimeEntry
|
||||
activeTimer *ticket.ActiveTimer
|
||||
spentByID map[string]time.Duration
|
||||
width int
|
||||
height int
|
||||
status string
|
||||
statusIsError bool
|
||||
filter string
|
||||
keys KeyMap
|
||||
styles Styles
|
||||
version string
|
||||
allTickets []ticket.Ticket
|
||||
creatingProject bool
|
||||
pendingDeleteTitle string
|
||||
mode Mode
|
||||
tickets views.TicketList
|
||||
projectList list.Model
|
||||
statusMenu views.StatusMenu
|
||||
boardMenu views.BoardMenu
|
||||
boardFilter ticket.BoardFilter
|
||||
input textinput.Model
|
||||
report views.ReportView
|
||||
reportGroupBy ticket.GroupBy
|
||||
groupByMenu views.GroupByMenu
|
||||
timeEntries views.TimeEntriesView
|
||||
timeEntriesTicket string // ticket ID for ModeTimeEntries
|
||||
editingTimeEntry ticket.TimeEntry
|
||||
activeTimer *ticket.ActiveTimer
|
||||
spentByID map[string]time.Duration
|
||||
width int
|
||||
height int
|
||||
status string
|
||||
statusIsError bool
|
||||
filter string
|
||||
keys KeyMap
|
||||
styles Styles
|
||||
version string
|
||||
allTickets []ticket.Ticket
|
||||
creatingProject bool
|
||||
pendingDeleteTitle string
|
||||
pendingDeleteProject string // non-empty → confirm deletes a project, not a ticket
|
||||
}
|
||||
|
||||
// New creates the root UI model.
|
||||
@@ -449,12 +450,31 @@ func (m Model) updateBoard(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
func (m Model) updateConfirmDelete(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
switch {
|
||||
case key.Matches(msg, m.keys.Escape):
|
||||
wasProject := m.pendingDeleteProject != ""
|
||||
m.pendingDeleteTitle = ""
|
||||
m.mode = ModeList
|
||||
m.pendingDeleteProject = ""
|
||||
m.clearStatus()
|
||||
if wasProject {
|
||||
m.mode = ModeProjects
|
||||
} else {
|
||||
m.mode = ModeList
|
||||
}
|
||||
return m, nil
|
||||
|
||||
case key.Matches(msg, m.keys.Enter), key.Matches(msg, m.keys.ConfirmYes):
|
||||
if m.pendingDeleteProject != "" {
|
||||
id := m.pendingDeleteProject
|
||||
m.pendingDeleteTitle = ""
|
||||
m.pendingDeleteProject = ""
|
||||
if err := m.deleteProject(id); err != nil {
|
||||
m.setError(err)
|
||||
m.mode = ModeProjects
|
||||
return m, nil
|
||||
}
|
||||
m.mode = ModeList
|
||||
m.setStatus(fmt.Sprintf("deleted project %s", id))
|
||||
return m, nil
|
||||
}
|
||||
m.pendingDeleteTitle = ""
|
||||
m.mode = ModeList
|
||||
if err := m.deleteSelected(); err != nil {
|
||||
@@ -712,6 +732,38 @@ func (m Model) updateProjects(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
m.input.Focus()
|
||||
return m, textinput.Blink
|
||||
|
||||
case key.Matches(msg, m.keys.DeleteForce):
|
||||
m.clearStatus()
|
||||
item, ok := m.projectList.SelectedItem().(views.ProjectItem)
|
||||
if !ok {
|
||||
m.setError(fmt.Errorf("no project selected"))
|
||||
return m, nil
|
||||
}
|
||||
if err := m.deleteProject(item.Project.ID); err != nil {
|
||||
m.setError(err)
|
||||
return m, nil
|
||||
}
|
||||
m.mode = ModeList
|
||||
m.setStatus(fmt.Sprintf("deleted project %s", item.Project.ID))
|
||||
return m, nil
|
||||
|
||||
case key.Matches(msg, m.keys.Delete):
|
||||
m.clearStatus()
|
||||
item, ok := m.projectList.SelectedItem().(views.ProjectItem)
|
||||
if !ok {
|
||||
m.setError(fmt.Errorf("no project selected"))
|
||||
return m, nil
|
||||
}
|
||||
n, err := m.countTickets(item.Project.ID)
|
||||
if err != nil {
|
||||
m.setError(err)
|
||||
return m, nil
|
||||
}
|
||||
m.pendingDeleteProject = item.Project.ID
|
||||
m.pendingDeleteTitle = fmt.Sprintf("project %q (%d tickets)", item.Project.ID, n)
|
||||
m.mode = ModeConfirmDelete
|
||||
return m, nil
|
||||
|
||||
case key.Matches(msg, m.keys.Enter):
|
||||
item, ok := m.projectList.SelectedItem().(views.ProjectItem)
|
||||
if !ok {
|
||||
@@ -942,6 +994,80 @@ func (m *Model) switchProject(id string) error {
|
||||
return m.reloadTickets()
|
||||
}
|
||||
|
||||
func (m *Model) countTickets(projectID string) (int, error) {
|
||||
tickets, err := m.store.List(projectID)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return len(tickets), nil
|
||||
}
|
||||
|
||||
func (m *Model) deleteProject(id string) error {
|
||||
if id == "" {
|
||||
return fmt.Errorf("no project selected")
|
||||
}
|
||||
all, err := m.projects.List()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(all) <= 1 {
|
||||
return fmt.Errorf("cannot delete the last project")
|
||||
}
|
||||
|
||||
wasActive := m.cfg.ActiveProject == id
|
||||
|
||||
// Clear running timer if it belongs to this project.
|
||||
if m.activeTimer != nil && m.activeTimer.ProjectID == id && m.timeStore != nil {
|
||||
if err := m.timeStore.SaveActiveTimer(nil); err != nil {
|
||||
return err
|
||||
}
|
||||
m.activeTimer = nil
|
||||
}
|
||||
|
||||
if m.timeStore != nil {
|
||||
if err := m.timeStore.DeleteByProject(id); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err := m.projects.Delete(id); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
m.cfg.ClearProjectLists(id)
|
||||
if wasActive {
|
||||
next := pickFallbackProject(all, id)
|
||||
m.cfg.ActiveProject = next
|
||||
m.boardFilter = ticket.ParseBoardLists(m.cfg.ActiveBoardLists(next))
|
||||
m.filter = ""
|
||||
}
|
||||
if err := config.Save(m.cfg); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := m.openProjects(); err != nil {
|
||||
return err
|
||||
}
|
||||
return m.reloadTickets()
|
||||
}
|
||||
|
||||
func pickFallbackProject(all []project.Project, deletedID string) string {
|
||||
var rest []string
|
||||
for _, p := range all {
|
||||
if p.ID == deletedID {
|
||||
continue
|
||||
}
|
||||
rest = append(rest, p.ID)
|
||||
}
|
||||
for _, id := range rest {
|
||||
if id == config.DefaultProject {
|
||||
return id
|
||||
}
|
||||
}
|
||||
if len(rest) > 0 {
|
||||
return rest[0]
|
||||
}
|
||||
return config.DefaultProject
|
||||
}
|
||||
|
||||
func (m *Model) openProjects() error {
|
||||
projects, err := m.projects.List()
|
||||
if err != nil {
|
||||
|
||||
Reference in New Issue
Block a user