diff --git a/pkg/tui/styles/styles.go b/pkg/tui/styles/styles.go index 53b3dfc..427513f 100644 --- a/pkg/tui/styles/styles.go +++ b/pkg/tui/styles/styles.go @@ -21,7 +21,7 @@ var ( ) const ( - ListViewRatio = 70 // Percentage of total width allocated to the list view + ListViewRatio = 55 // Percentage of total width allocated to the list view LabelWidth = 14 MarginBottomSmall = 1 ) diff --git a/pkg/tui/views/dashboard/help_bar.go b/pkg/tui/views/dashboard/help_bar.go index 39a1949..5546e18 100644 --- a/pkg/tui/views/dashboard/help_bar.go +++ b/pkg/tui/views/dashboard/help_bar.go @@ -36,7 +36,7 @@ func (h HelpBar) shortHelp() string { styles.HighlightKey("↓/j") + "down ", styles.HighlightKey("pgup/pgdn") + "page ", styles.HighlightKey("a") + "add ", - styles.HighlightKey("e/d/s/p/t/c") + "edit ", + styles.HighlightKey("e/d/s/p/t/c/A") + "edit ", styles.HighlightKey("x") + "delete ", styles.HighlightKey("q") + "quit ", styles.HighlightKey("S") + "submit ", @@ -78,8 +78,9 @@ func (h HelpBar) fullHelp() string { renderRow("e", "edit title", "d", "edit desc"), renderRow("c", "add comment", "", ""), renderRow("s", "change status", "p", "change priority"), - renderRow("t", "change type", "x", "delete issue"), - renderRow("S", "submit task", "q", "quit"), + renderRow("t", "change type", "A", "change assignee"), + renderRow("x", "delete issue", "S", "submit task"), + renderRow("q", "quit", "", ""), } content := lipgloss.JoinVertical(lipgloss.Left, rows...) return lipgloss.NewStyle(). diff --git a/pkg/tui/views/dashboard/issue_detail.go b/pkg/tui/views/dashboard/issue_detail.go index 2899bca..6ca02a6 100644 --- a/pkg/tui/views/dashboard/issue_detail.go +++ b/pkg/tui/views/dashboard/issue_detail.go @@ -66,11 +66,19 @@ func (i *IssueDetail) refreshContent() { styles.LabelStyle.Render("Priority:") + styles.ValueStyle.Render(priorityCodeName(i.issue.Priority)), ) + assignee := i.issue.Assignee + if assignee == "" { + assignee = "—" + } + assigneeRow := styles.RowStyle.Render( + styles.LabelStyle.Render("Assignee:") + styles.ValueStyle.Render(assignee), + ) + descLabel := styles.LabelStyle.Render("Description:") descContent := styles.ValueStyle.Render(i.issue.Description) var parts []string - parts = append(parts, titleRow, idRow, typeRow, statusRow, priorityRow, descLabel, descContent) + parts = append(parts, titleRow, idRow, typeRow, statusRow, priorityRow, assigneeRow, descLabel, descContent) // Comments section commentsLabel := styles.LabelStyle.Render("Comments:") diff --git a/pkg/tui/views/dashboard/issue_list.go b/pkg/tui/views/dashboard/issue_list.go index c2eb4cb..e324258 100644 --- a/pkg/tui/views/dashboard/issue_list.go +++ b/pkg/tui/views/dashboard/issue_list.go @@ -47,15 +47,16 @@ func getTableColumns(width int) []TableColumn { return []TableColumn{ {width: 10, label: "ID", key: "id"}, {width: 20, label: "TITLE", key: "title"}, - {width: 15, label: "STATUS", key: "status"}, + {width: 13, label: "STATUS", key: "status"}, } default: return []TableColumn{ - {width: 12, label: "ID", key: "id"}, - {width: 20, label: "TITLE", key: "title"}, - {width: 15, label: "STATUS", key: "status"}, - {width: 10, label: "TYPE", key: "type"}, - {width: 15, label: "PRIORITY", key: "priority"}, + {width: 10, label: "ID", key: "id"}, + {width: 24, label: "TITLE", key: "title"}, + {width: 14, label: "STATUS", key: "status"}, + {width: 11, label: "TYPE", key: "type"}, + {width: 13, label: "PRIORITY", key: "priority"}, + {width: 13, label: "ASSIGNEE", key: "assignee"}, } } } @@ -340,6 +341,8 @@ func getColumnValue(col TableColumn, issue ListIssue) string { return string(issue.Issue.IssueType) case "priority": return priorityCodeName(issue.Issue.Priority) + case "assignee": + return issue.Issue.Assignee default: return "" } diff --git a/pkg/tui/views/dashboard/keys.go b/pkg/tui/views/dashboard/keys.go index dd0981b..a2deace 100644 --- a/pkg/tui/views/dashboard/keys.go +++ b/pkg/tui/views/dashboard/keys.go @@ -18,6 +18,7 @@ type DashboardKeyMap struct { ChangeStatus key.Binding ChangePriority key.Binding ChangeType key.Binding + ChangeAssignee key.Binding AddComment key.Binding AddIssue key.Binding DeleteIssue key.Binding @@ -73,6 +74,10 @@ var defaultDashboardKeyMap = DashboardKeyMap{ key.WithKeys("t"), key.WithHelp("t", "change type"), ), + ChangeAssignee: key.NewBinding( + key.WithKeys("A"), + key.WithHelp("A", "change assignee"), + ), AddComment: key.NewBinding( key.WithKeys("c"), key.WithHelp("c", "add comment"), @@ -116,43 +121,49 @@ func (d *Model) handleKeyMsg(msg tea.KeyMsg) tea.Cmd { case d.IsFocusedOnDetail() && key.Matches(msg, d.keyMap.ScrollDown): d.issueDetail.ScrollDown(1) d.logAction("tui scrolled issue detail down") - case !d.editingTitle && !d.creatingIssue && !d.editingDescription && !d.choosingStatus && !d.choosingPriority && !d.confirmingDelete && !d.choosingType && !d.addingComment && key.Matches(msg, d.keyMap.EditTitle): + case !d.editingTitle && !d.creatingIssue && !d.editingDescription && !d.choosingStatus && !d.choosingPriority && !d.confirmingDelete && !d.choosingType && !d.editingAssignee && !d.addingComment && key.Matches(msg, d.keyMap.EditTitle): if selected := d.FocusedIssueList().SelectedItem(); selected.ID != "" { d.startEditTitle(selected) cmd = d.titleInput.Focus() d.logAction("tui started editing issue title") } - case !d.editingTitle && !d.creatingIssue && !d.editingDescription && !d.choosingStatus && !d.choosingPriority && !d.confirmingDelete && !d.choosingType && !d.addingComment && key.Matches(msg, d.keyMap.EditDescription): + case !d.editingTitle && !d.creatingIssue && !d.editingDescription && !d.choosingStatus && !d.choosingPriority && !d.confirmingDelete && !d.choosingType && !d.editingAssignee && !d.addingComment && key.Matches(msg, d.keyMap.EditDescription): if selected := d.FocusedIssueList().SelectedItem(); selected.ID != "" { d.startEditDescription(selected) cmd = d.descriptionInput.Focus() d.logAction("tui started editing issue description") } - case !d.editingTitle && !d.creatingIssue && !d.editingDescription && !d.choosingStatus && !d.choosingPriority && !d.confirmingDelete && !d.choosingType && !d.addingComment && key.Matches(msg, d.keyMap.ChangeStatus): + case !d.editingTitle && !d.creatingIssue && !d.editingDescription && !d.choosingStatus && !d.choosingPriority && !d.confirmingDelete && !d.choosingType && !d.editingAssignee && !d.addingComment && key.Matches(msg, d.keyMap.ChangeStatus): if selected := d.FocusedIssueList().SelectedItem(); selected.ID != "" { d.startChooseStatus(selected) d.logAction("tui opened status picker") } - case !d.editingTitle && !d.creatingIssue && !d.editingDescription && !d.choosingStatus && !d.choosingPriority && !d.confirmingDelete && !d.choosingType && !d.addingComment && key.Matches(msg, d.keyMap.ChangePriority): + case !d.editingTitle && !d.creatingIssue && !d.editingDescription && !d.choosingStatus && !d.choosingPriority && !d.confirmingDelete && !d.choosingType && !d.editingAssignee && !d.addingComment && key.Matches(msg, d.keyMap.ChangePriority): if selected := d.FocusedIssueList().SelectedItem(); selected.ID != "" { d.startChoosePriority(selected) d.logAction("tui opened priority picker") } - case !d.editingTitle && !d.creatingIssue && !d.editingDescription && !d.choosingStatus && !d.choosingPriority && !d.confirmingDelete && !d.choosingType && !d.addingComment && key.Matches(msg, d.keyMap.ChangeType): + case !d.editingTitle && !d.creatingIssue && !d.editingDescription && !d.choosingStatus && !d.choosingPriority && !d.confirmingDelete && !d.choosingType && !d.editingAssignee && !d.addingComment && key.Matches(msg, d.keyMap.ChangeType): if selected := d.FocusedIssueList().SelectedItem(); selected.ID != "" { d.startChooseType(selected) d.logAction("tui opened type picker") } - case !d.editingTitle && !d.creatingIssue && !d.editingDescription && !d.choosingStatus && !d.choosingPriority && !d.confirmingDelete && !d.choosingType && !d.addingComment && key.Matches(msg, d.keyMap.AddComment): + case !d.editingTitle && !d.creatingIssue && !d.editingDescription && !d.choosingStatus && !d.choosingPriority && !d.confirmingDelete && !d.choosingType && !d.editingAssignee && !d.addingComment && key.Matches(msg, d.keyMap.ChangeAssignee): + if selected := d.FocusedIssueList().SelectedItem(); selected.ID != "" { + d.startEditAssignee(selected) + cmd = d.assigneeInput.Focus() + d.logAction("tui started editing assignee") + } + case !d.editingTitle && !d.creatingIssue && !d.editingDescription && !d.choosingStatus && !d.choosingPriority && !d.confirmingDelete && !d.choosingType && !d.editingAssignee && !d.addingComment && key.Matches(msg, d.keyMap.AddComment): if selected := d.FocusedIssueList().SelectedItem(); selected.ID != "" { d.startAddComment(selected) cmd = d.commentInput.Focus() } - case !d.editingTitle && !d.creatingIssue && !d.editingDescription && !d.choosingStatus && !d.choosingPriority && !d.confirmingDelete && !d.choosingType && !d.addingComment && key.Matches(msg, d.keyMap.AddIssue): + case !d.editingTitle && !d.creatingIssue && !d.editingDescription && !d.choosingStatus && !d.choosingPriority && !d.confirmingDelete && !d.choosingType && !d.editingAssignee && !d.addingComment && key.Matches(msg, d.keyMap.AddIssue): d.startCreateIssue() cmd = d.createTitleInput.Focus() d.logAction("tui started creating issue") - case !d.editingTitle && !d.creatingIssue && !d.editingDescription && !d.choosingStatus && !d.choosingPriority && !d.confirmingDelete && !d.choosingType && !d.addingComment && key.Matches(msg, d.keyMap.DeleteIssue): + case !d.editingTitle && !d.creatingIssue && !d.editingDescription && !d.choosingStatus && !d.choosingPriority && !d.confirmingDelete && !d.choosingType && !d.editingAssignee && !d.addingComment && key.Matches(msg, d.keyMap.DeleteIssue): fl := d.FocusedIssueList() if selected := fl.SelectedItem(); selected.ID != "" { d.startConfirmDelete(selected.ID, fl.Index()) diff --git a/pkg/tui/views/dashboard/model.go b/pkg/tui/views/dashboard/model.go index de1ce4b..33a2275 100644 --- a/pkg/tui/views/dashboard/model.go +++ b/pkg/tui/views/dashboard/model.go @@ -47,6 +47,9 @@ type Model struct { priorityIssueID string choosingType bool // true while choosing a type typeIssueID string + editingAssignee bool // true while editing assignee + assigneeInput textinput.Model + assigneeIssueID string addingComment bool // true while adding a comment commentInput textarea.Model commentIssueID string @@ -88,6 +91,11 @@ func NewDashboard(app *app.App, feedbackChan chan models.ValidationFeedback, qui createTi.CharLimit = 256 m.createTitleInput = createTi + assigneeTi := textinput.New() + assigneeTi.Placeholder = "Assignee (e.g. Me, username)..." + assigneeTi.CharLimit = 128 + m.assigneeInput = assigneeTi + descTa := textarea.New() descTa.Placeholder = "Issue description..." descTa.SetWidth(56) @@ -177,6 +185,13 @@ func (m *Model) startChooseType(selected ListIssue) { m.typeIssueID = selected.ID } +func (m *Model) startEditAssignee(selected ListIssue) { + m.editingAssignee = true + m.assigneeIssueID = selected.ID + m.assigneeInput.SetValue(selected.Issue.Assignee) + m.assigneeInput.CursorEnd() +} + func (m *Model) Init() tea.Cmd { return m.listenForValidation() } diff --git a/pkg/tui/views/dashboard/operations.go b/pkg/tui/views/dashboard/operations.go index 566692e..6046821 100644 --- a/pkg/tui/views/dashboard/operations.go +++ b/pkg/tui/views/dashboard/operations.go @@ -49,6 +49,11 @@ type issueTypeUpdatedMsg struct { Err error } +type issueAssigneeUpdatedMsg struct { + IssueID string + Err error +} + type selectIssueMsg struct { IssueID string } @@ -116,6 +121,14 @@ func updateIssueTypeCmd(app *app.App, issueID string, issueType models.IssueType } } +func updateIssueAssigneeCmd(app *app.App, issueID, assignee string) tea.Cmd { + return func() tea.Msg { + updates := map[string]interface{}{"assignee": assignee} + err := app.Issues.UpdateIssue(context.Background(), issueID, updates, "tui") + return issueAssigneeUpdatedMsg{IssueID: issueID, Err: err} + } +} + func createIssueCmd(app *app.App, title string) tea.Cmd { return func() tea.Msg { issue := &models.Issue{ @@ -209,6 +222,17 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.logAction("tui updated issue type") return m, m.refreshIssueListsAndSelectIssue(msg.IssueID) + case issueAssigneeUpdatedMsg: + m.editingAssignee = false + m.assigneeIssueID = "" + m.assigneeInput.Blur() + if msg.Err != nil { + m.logAction("tui failed to update issue assignee") + return m, nil + } + m.logAction("tui updated issue assignee") + return m, m.refreshIssueListsAndSelectIssue(msg.IssueID) + case selectIssueMsg: m.issueList.SelectIssueID(msg.IssueID) m.closedIssueList.SelectIssueID(msg.IssueID) @@ -480,6 +504,27 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, cmd } + if m.editingAssignee { + if msg.String() == "enter" { + assignee := m.assigneeInput.Value() + issueID := m.assigneeIssueID + m.editingAssignee = false + m.assigneeIssueID = "" + m.assigneeInput.Blur() + return m, updateIssueAssigneeCmd(m.app, issueID, assignee) + } + if msg.String() == "esc" { + m.logAction("tui canceled assignee edit") + m.editingAssignee = false + m.assigneeIssueID = "" + m.assigneeInput.Blur() + return m, nil + } + var cmd tea.Cmd + m.assigneeInput, cmd = m.assigneeInput.Update(msg) + return m, cmd + } + if m.editingDescription { if msg.String() == "ctrl+s" { m.logAction("tui submitted issue description edit") diff --git a/pkg/tui/views/dashboard/view.go b/pkg/tui/views/dashboard/view.go index 74aca51..0d2d14f 100644 --- a/pkg/tui/views/dashboard/view.go +++ b/pkg/tui/views/dashboard/view.go @@ -56,6 +56,20 @@ func (m *Model) View() string { mainView := lipgloss.JoinVertical(lipgloss.Left, header, content, footer) + if m.editingAssignee { + editBoxWidth := min(60, m.width-4) + m.assigneeInput.Width = editBoxWidth - 2 + editContent := lipgloss.JoinVertical(lipgloss.Left, + styles.LabelStyle.Render("Change assignee for "+m.assigneeIssueID+" (Enter to save, Esc to cancel):"), + m.assigneeInput.View(), + ) + editBox := styles.ContainerStyle. + Width(editBoxWidth). + BorderForeground(styles.PrimaryBorder). + Render(editContent) + return lipgloss.Place(m.width, m.height, lipgloss.Center, lipgloss.Center, editBox) + } + if m.editingTitle { editBoxWidth := min(60, m.width-4) m.titleInput.Width = editBoxWidth - 2