Allow moving tickets between projects with P.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Skruecken
2026-10-19 14:19:10 +02:00
co-authored by Cursor
parent c5920ffd54
commit 7666715f6c
13 changed files with 369 additions and 6 deletions
+84 -2
View File
@@ -39,6 +39,7 @@ const (
ModeCreate
ModeSearch
ModeProjects
ModeMoveTicket
ModeStatus
ModeBoard
ModeConfirmDelete
@@ -166,6 +167,8 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return m.updateInput(msg)
case ModeProjects:
return m.updateProjects(msg)
case ModeMoveTicket:
return m.updateMoveTicket(msg)
case ModeStatus:
return m.updateStatus(msg)
case ModeBoard:
@@ -185,7 +188,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
var cmd tea.Cmd
switch m.mode {
case ModeProjects:
case ModeProjects, ModeMoveTicket:
m.projectList, cmd = m.projectList.Update(msg)
case ModeCreate, ModeSearch, ModeTimeManual, ModeTimeEdit:
m.input, cmd = m.input.Update(msg)
@@ -216,7 +219,7 @@ func (m *Model) resizeLists() {
bodyH = 1
}
m.tickets.SetSize(m.width, bodyH)
if m.mode == ModeProjects {
if m.mode == ModeProjects || m.mode == ModeMoveTicket {
m.projectList.SetSize(m.width, bodyH)
}
m.input.Width = max(10, m.width-4)
@@ -258,6 +261,19 @@ func (m Model) updateList(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
m.mode = ModeProjects
return m, nil
case key.Matches(msg, m.keys.MoveTicket):
m.clearStatus()
if _, ok := m.tickets.Selected(); !ok {
m.setError(fmt.Errorf("no ticket selected"))
return m, nil
}
if err := m.openProjects(); err != nil {
m.setError(err)
return m, nil
}
m.mode = ModeMoveTicket
return m, nil
case key.Matches(msg, m.keys.Board):
m.clearStatus()
m.boardMenu = views.NewBoardMenu(m.boardFilter, m.width, views.StatusMenuStyles{
@@ -716,6 +732,37 @@ func (m Model) updateReportGroup(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
return m, nil
}
func (m Model) updateMoveTicket(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
switch {
case key.Matches(msg, m.keys.Escape):
m.mode = ModeList
m.clearStatus()
return m, nil
case key.Matches(msg, m.keys.Enter):
item, ok := m.projectList.SelectedItem().(views.ProjectItem)
if !ok {
return m, nil
}
if err := m.moveSelectedToProject(item.Project.ID); err != nil {
m.setError(err)
return m, nil
}
m.mode = ModeList
return m, nil
}
if msg.Type == tea.KeyRunes && len(msg.Runes) == 1 {
if views.JumpProjectByLetter(&m.projectList, msg.Runes[0]) {
return m, nil
}
}
var cmd tea.Cmd
m.projectList, cmd = m.projectList.Update(msg)
return m, cmd
}
func (m Model) updateProjects(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
switch {
case key.Matches(msg, m.keys.Escape):
@@ -994,6 +1041,41 @@ func (m *Model) switchProject(id string) error {
return m.reloadTickets()
}
func (m *Model) moveSelectedToProject(toProjectID string) error {
t, ok := m.tickets.Selected()
if !ok {
return fmt.Errorf("no ticket selected")
}
if toProjectID == t.ProjectID {
m.setStatus("already there")
return nil
}
if err := m.projects.Ensure(toProjectID); err != nil {
return err
}
after := t
after.ProjectID = toProjectID
if err := m.exec.Do(&command.MoveTicket{
Store: m.store,
TimeStore: m.timeStore,
Before: t,
After: after,
}); err != nil {
return err
}
if m.activeTimer != nil && m.activeTimer.TicketID == t.ID && m.timeStore != nil {
m.activeTimer.ProjectID = toProjectID
if err := m.timeStore.SaveActiveTimer(m.activeTimer); err != nil {
return err
}
}
if err := m.reloadTickets(); err != nil {
return err
}
m.setStatus(fmt.Sprintf("moved → %s", toProjectID))
return nil
}
func (m *Model) countTickets(projectID string) (int, error) {
tickets, err := m.store.List(projectID)
if err != nil {