copied the dashboard and made a keybinding to change back and forth between them; ready to turn dashboard2 into a boardview dashboard
This commit is contained in:
7
pkg/tui/msgs/msgs.go
Normal file
7
pkg/tui/msgs/msgs.go
Normal file
@@ -0,0 +1,7 @@
|
||||
package msgs
|
||||
|
||||
// SwitchToDashboardMsg signals to switch to the main dashboard view.
|
||||
type SwitchToDashboardMsg struct{}
|
||||
|
||||
// SwitchToDashboard2Msg signals to switch to the dashboard2 view.
|
||||
type SwitchToDashboard2Msg struct{}
|
||||
@@ -29,7 +29,7 @@ func (t *Tui) Run(ctx context.Context, config Config) error {
|
||||
|
||||
defer cleanup()
|
||||
|
||||
p := tea.NewProgram(views.NewDashboardView(app, t.feedbackChan, t.quitChan),
|
||||
p := tea.NewProgram(views.NewRootView(app, t.feedbackChan, t.quitChan),
|
||||
tea.WithAltScreen(), tea.WithMouseAllMotion())
|
||||
|
||||
if t.quitChan != nil {
|
||||
|
||||
@@ -32,6 +32,7 @@ func (h HelpBar) View() string {
|
||||
func (h HelpBar) shortHelp() string {
|
||||
keys := []string{
|
||||
styles.HighlightKey("tab") + " switch",
|
||||
styles.HighlightKey("ctrl+2") + " dash2",
|
||||
styles.HighlightKey("↑/k") + " up",
|
||||
styles.HighlightKey("↓/j") + " down",
|
||||
styles.HighlightKey("pgup/pgdn") + " page",
|
||||
@@ -74,10 +75,11 @@ func (h HelpBar) fullHelp() string {
|
||||
renderRow("enter", "view issue", "↓/j", "down"),
|
||||
renderRow("pgup", "page up", "pgdn", "page down"),
|
||||
renderRow("b", "back to list", "a", "add issue"),
|
||||
renderRow("e", "edit title", "d", "edit desc"),
|
||||
renderRow("e", "edit title", "d", "edit description"),
|
||||
renderRow("s", "change status", "p", "change priority"),
|
||||
renderRow("t", "change type", "x", "delete issue"),
|
||||
renderRow("?", "help", "q", "quit"),
|
||||
renderRow("ctrl+2", "dashboard 2", "q", "quit"),
|
||||
renderRow("?", "help", "", ""),
|
||||
}
|
||||
content := lipgloss.JoinVertical(lipgloss.Left, rows...)
|
||||
return lipgloss.NewStyle().
|
||||
|
||||
@@ -1,25 +1,27 @@
|
||||
package dashboard
|
||||
|
||||
import (
|
||||
"github.com/LazyBachelor/LazyPM/pkg/tui/msgs"
|
||||
"github.com/charmbracelet/bubbles/key"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
)
|
||||
|
||||
type DashboardKeyMap struct {
|
||||
Help key.Binding
|
||||
Quit key.Binding
|
||||
SelectIssue key.Binding
|
||||
BackToList key.Binding
|
||||
ScrollUp key.Binding
|
||||
ScrollDown key.Binding
|
||||
SwitchWindow key.Binding
|
||||
EditTitle key.Binding
|
||||
EditDescription key.Binding
|
||||
ChangeStatus key.Binding
|
||||
ChangePriority key.Binding
|
||||
ChangeType key.Binding
|
||||
AddIssue key.Binding
|
||||
DeleteIssue key.Binding
|
||||
Help key.Binding
|
||||
Quit key.Binding
|
||||
SelectIssue key.Binding
|
||||
BackToList key.Binding
|
||||
ScrollUp key.Binding
|
||||
ScrollDown key.Binding
|
||||
SwitchWindow key.Binding
|
||||
SwitchToDashboard2 key.Binding
|
||||
EditTitle key.Binding
|
||||
EditDescription key.Binding
|
||||
ChangeStatus key.Binding
|
||||
ChangePriority key.Binding
|
||||
ChangeType key.Binding
|
||||
AddIssue key.Binding
|
||||
DeleteIssue key.Binding
|
||||
}
|
||||
|
||||
var defaultDashboardKeyMap = DashboardKeyMap{
|
||||
@@ -51,6 +53,10 @@ var defaultDashboardKeyMap = DashboardKeyMap{
|
||||
key.WithKeys("tab"),
|
||||
key.WithHelp("tab", "switch window"),
|
||||
),
|
||||
SwitchToDashboard2: key.NewBinding(
|
||||
key.WithKeys("2"),
|
||||
key.WithHelp("2", "dashboard 2"),
|
||||
),
|
||||
EditTitle: key.NewBinding(
|
||||
key.WithKeys("e"),
|
||||
key.WithHelp("e", "edit title"),
|
||||
@@ -91,6 +97,8 @@ func (d *Model) handleKeyMsg(msg tea.KeyMsg) tea.Cmd {
|
||||
return tea.Quit
|
||||
case key.Matches(msg, d.keyMap.SwitchWindow):
|
||||
d.ToggleFocusedWindow()
|
||||
case !d.editingTitle && !d.creatingIssue && !d.editingDescription && !d.choosingStatus && !d.choosingPriority && !d.confirmingDelete && !d.choosingType && key.Matches(msg, d.keyMap.SwitchToDashboard2):
|
||||
return func() tea.Msg { return msgs.SwitchToDashboard2Msg{} }
|
||||
case d.IsFocusedOnList() && key.Matches(msg, d.keyMap.SelectIssue):
|
||||
d.FocusDetail()
|
||||
case d.IsFocusedOnDetail() && key.Matches(msg, d.keyMap.BackToList):
|
||||
|
||||
32
pkg/tui/views/dashboard2/header.go
Normal file
32
pkg/tui/views/dashboard2/header.go
Normal file
@@ -0,0 +1,32 @@
|
||||
package dashboard2
|
||||
|
||||
import (
|
||||
"github.com/LazyBachelor/LazyPM/pkg/tui/styles"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
)
|
||||
|
||||
type Header struct {
|
||||
title string
|
||||
}
|
||||
|
||||
func NewHeader(title string) Header {
|
||||
return Header{
|
||||
title: title,
|
||||
}
|
||||
}
|
||||
|
||||
func (h Header) View(width int) string {
|
||||
title := styles.HeaderTitleStyle.Render(h.title)
|
||||
|
||||
return lipgloss.PlaceHorizontal(
|
||||
width,
|
||||
lipgloss.Left,
|
||||
title,
|
||||
lipgloss.WithWhitespaceChars("─"),
|
||||
lipgloss.WithWhitespaceForeground(styles.Primary),
|
||||
)
|
||||
}
|
||||
|
||||
func (h Header) Height() int {
|
||||
return 1
|
||||
}
|
||||
106
pkg/tui/views/dashboard2/help_bar.go
Normal file
106
pkg/tui/views/dashboard2/help_bar.go
Normal file
@@ -0,0 +1,106 @@
|
||||
package dashboard2
|
||||
|
||||
import (
|
||||
"github.com/LazyBachelor/LazyPM/pkg/tui/styles"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
)
|
||||
|
||||
type HelpBar struct {
|
||||
keyMap DashboardKeyMap
|
||||
showAll bool
|
||||
width int
|
||||
}
|
||||
|
||||
func NewHelpBar(keyMap DashboardKeyMap) HelpBar {
|
||||
return HelpBar{keyMap: keyMap}
|
||||
}
|
||||
|
||||
func (h *HelpBar) SetWidth(width int) {
|
||||
h.width = width
|
||||
}
|
||||
|
||||
func (h HelpBar) View() string {
|
||||
if h.width == 0 {
|
||||
return ""
|
||||
}
|
||||
if h.showAll {
|
||||
return h.fullHelp()
|
||||
}
|
||||
return h.shortHelp()
|
||||
}
|
||||
|
||||
func (h HelpBar) shortHelp() string {
|
||||
keys := []string{
|
||||
styles.HighlightKey("tab") + " switch",
|
||||
styles.HighlightKey("ctrl+1") + " dash1",
|
||||
styles.HighlightKey("↑/k") + " up",
|
||||
styles.HighlightKey("↓/j") + " down",
|
||||
styles.HighlightKey("pgup/pgdn") + " page",
|
||||
styles.HighlightKey("a") + " add",
|
||||
styles.HighlightKey("e/d/s/p/t") + " edit",
|
||||
styles.HighlightKey("x") + " delete",
|
||||
styles.HighlightKey("q") + " quit",
|
||||
styles.HighlightKey("?") + " help",
|
||||
}
|
||||
content := lipgloss.JoinHorizontal(lipgloss.Left, keys...)
|
||||
return lipgloss.NewStyle().
|
||||
Border(lipgloss.Border{Top: "─"}, true, false, false, false).
|
||||
BorderForeground(styles.SecondaryBorder).
|
||||
Padding(0, 1).
|
||||
Width(h.width).
|
||||
Render(content)
|
||||
}
|
||||
|
||||
func (h HelpBar) fullHelp() string {
|
||||
keyStyle := lipgloss.NewStyle().Width(8).Align(lipgloss.Right)
|
||||
descStyle := lipgloss.NewStyle().Width(12)
|
||||
|
||||
renderHelpItem := func(key, desc string) string {
|
||||
return lipgloss.JoinHorizontal(
|
||||
lipgloss.Left,
|
||||
keyStyle.Render(styles.HighlightKey(key)),
|
||||
" ",
|
||||
descStyle.Render(desc),
|
||||
)
|
||||
}
|
||||
|
||||
renderRow := func(leftKey, leftDesc, rightKey, rightDesc string) string {
|
||||
leftItem := renderHelpItem(leftKey, leftDesc)
|
||||
rightItem := renderHelpItem(rightKey, rightDesc)
|
||||
return lipgloss.JoinHorizontal(lipgloss.Left, leftItem, " ", rightItem)
|
||||
}
|
||||
|
||||
rows := []string{
|
||||
renderRow("tab", "switch window", "↑/k", "up"),
|
||||
renderRow("enter", "view issue", "↓/j", "down"),
|
||||
renderRow("pgup", "page up", "pgdn", "page down"),
|
||||
renderRow("b", "back to list", "a", "add issue"),
|
||||
renderRow("e", "edit title", "d", "edit description"),
|
||||
renderRow("s", "change status", "p", "change priority"),
|
||||
renderRow("t", "change type", "x", "delete issue"),
|
||||
renderRow("ctrl+1", "dashboard 1", "q", "quit"),
|
||||
renderRow("?", "help", "", ""),
|
||||
}
|
||||
content := lipgloss.JoinVertical(lipgloss.Left, rows...)
|
||||
return lipgloss.NewStyle().
|
||||
Border(lipgloss.Border{Top: "─"}, true, false, false, false).
|
||||
BorderForeground(styles.SecondaryBorder).
|
||||
Padding(0, 1).
|
||||
Width(h.width).
|
||||
Render(content)
|
||||
}
|
||||
|
||||
func (h HelpBar) Height() int {
|
||||
if h.width == 0 {
|
||||
return 0
|
||||
}
|
||||
return lipgloss.Height(h.View())
|
||||
}
|
||||
|
||||
func (h HelpBar) IsExpanded() bool {
|
||||
return h.showAll
|
||||
}
|
||||
|
||||
func (h *HelpBar) ToggleHelp() {
|
||||
h.showAll = !h.showAll
|
||||
}
|
||||
100
pkg/tui/views/dashboard2/issue_detail.go
Normal file
100
pkg/tui/views/dashboard2/issue_detail.go
Normal file
@@ -0,0 +1,100 @@
|
||||
package dashboard2
|
||||
|
||||
import (
|
||||
"github.com/LazyBachelor/LazyPM/internal/models"
|
||||
"github.com/LazyBachelor/LazyPM/pkg/tui/styles"
|
||||
"github.com/charmbracelet/bubbles/viewport"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
)
|
||||
|
||||
type IssueDetail struct {
|
||||
viewport viewport.Model
|
||||
issue models.Issue
|
||||
focused bool
|
||||
}
|
||||
|
||||
func NewIssueDetail() IssueDetail {
|
||||
vp := viewport.New(0, 0)
|
||||
return IssueDetail{
|
||||
viewport: vp,
|
||||
}
|
||||
}
|
||||
|
||||
func (i *IssueDetail) SetIssue(issue models.Issue) {
|
||||
i.issue = issue
|
||||
i.refreshContent()
|
||||
}
|
||||
|
||||
func (i *IssueDetail) SetSize(width, height int) {
|
||||
i.viewport.Height = height
|
||||
i.viewport.Width = width
|
||||
i.refreshContent()
|
||||
}
|
||||
|
||||
func (i *IssueDetail) SetFocused(focused bool) {
|
||||
i.focused = focused
|
||||
}
|
||||
|
||||
func (i *IssueDetail) refreshContent() {
|
||||
|
||||
titleRow := styles.RowStyle.Render(
|
||||
styles.TitleStyle.Render(i.issue.Title),
|
||||
)
|
||||
|
||||
idRow := styles.RowStyle.Render(
|
||||
styles.LabelStyle.Render("ID:") + styles.ValueStyle.Render(i.issue.ID),
|
||||
)
|
||||
|
||||
typeRow := styles.RowStyle.Render(
|
||||
styles.LabelStyle.Render("Type:") + styles.ValueStyle.Render(string(i.issue.IssueType)),
|
||||
)
|
||||
|
||||
statusRow := styles.RowStyle.Render(
|
||||
styles.LabelStyle.Render("Status:") + styles.StatusStyle(string(i.issue.Status)).Render(string(i.issue.Status)),
|
||||
)
|
||||
|
||||
priorityRow := styles.RowStyle.Render(
|
||||
styles.LabelStyle.Render("Priority:") + styles.ValueStyle.Render(priorityCodeName(i.issue.Priority)),
|
||||
)
|
||||
|
||||
descLabel := styles.LabelStyle.Render("Description:")
|
||||
descContent := styles.ValueStyle.Render(i.issue.Description)
|
||||
|
||||
content := lipgloss.JoinVertical(lipgloss.Left,
|
||||
titleRow,
|
||||
idRow,
|
||||
typeRow,
|
||||
statusRow,
|
||||
priorityRow,
|
||||
descLabel,
|
||||
descContent,
|
||||
)
|
||||
|
||||
i.viewport.SetContent(content)
|
||||
}
|
||||
|
||||
func (i IssueDetail) View() string {
|
||||
content := i.viewport.View()
|
||||
|
||||
if i.focused {
|
||||
return styles.DetailsContainerStyle.
|
||||
BorderForeground(styles.PrimaryBorder).
|
||||
Width(i.viewport.Width).
|
||||
Height(i.viewport.Height).
|
||||
MaxHeight(i.viewport.Height).
|
||||
Render(content)
|
||||
}
|
||||
return styles.DetailsContainerStyle.
|
||||
Width(i.viewport.Width).
|
||||
Height(i.viewport.Height).
|
||||
MaxHeight(i.viewport.Height).
|
||||
Render(content)
|
||||
}
|
||||
|
||||
func (i *IssueDetail) ScrollUp(lines int) {
|
||||
i.viewport.ScrollUp(lines)
|
||||
}
|
||||
|
||||
func (i *IssueDetail) ScrollDown(lines int) {
|
||||
i.viewport.ScrollDown(lines)
|
||||
}
|
||||
346
pkg/tui/views/dashboard2/issue_list.go
Normal file
346
pkg/tui/views/dashboard2/issue_list.go
Normal file
@@ -0,0 +1,346 @@
|
||||
package dashboard2
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"sort"
|
||||
|
||||
"github.com/LazyBachelor/LazyPM/internal/app"
|
||||
"github.com/LazyBachelor/LazyPM/internal/models"
|
||||
"github.com/LazyBachelor/LazyPM/pkg/tui/styles"
|
||||
"github.com/charmbracelet/bubbles/list"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
"github.com/muesli/reflow/truncate"
|
||||
)
|
||||
|
||||
type IssueList struct {
|
||||
list list.Model
|
||||
app *app.App
|
||||
width int
|
||||
height int
|
||||
}
|
||||
|
||||
type ListIssue struct {
|
||||
models.Issue
|
||||
}
|
||||
|
||||
func (l ListIssue) Title() string { return l.Issue.Title }
|
||||
func (l ListIssue) Description() string { return l.Issue.Description }
|
||||
func (l ListIssue) FilterValue() string { return l.Issue.ID + " " + l.Issue.Title }
|
||||
|
||||
type TableColumn struct {
|
||||
width uint
|
||||
label string
|
||||
key string
|
||||
}
|
||||
|
||||
func getTableColumns(width int) []TableColumn {
|
||||
switch {
|
||||
case width < 45:
|
||||
return []TableColumn{
|
||||
{width: 10, label: "ID", key: "id"},
|
||||
{width: uint(width - 10), label: "TITLE", key: "title"},
|
||||
}
|
||||
case width < 60:
|
||||
return []TableColumn{
|
||||
{width: 10, label: "ID", key: "id"},
|
||||
{width: 20, label: "TITLE", key: "title"},
|
||||
{width: 15, 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"},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func renderHeaders(cols []TableColumn) string {
|
||||
var parts []string
|
||||
headerStyle := lipgloss.NewStyle().Foreground(styles.FaintText).Bold(true)
|
||||
|
||||
for _, col := range cols {
|
||||
colWidth := col.width
|
||||
if colWidth == 0 {
|
||||
colWidth = 1
|
||||
}
|
||||
style := lipgloss.NewStyle().Width(int(colWidth))
|
||||
headerText := headerStyle.Render(truncate.StringWithTail(col.label, colWidth, "..."))
|
||||
parts = append(parts, style.Render(headerText))
|
||||
}
|
||||
|
||||
return lipgloss.JoinHorizontal(lipgloss.Left, parts...)
|
||||
}
|
||||
|
||||
func NewIssueList(app *app.App, width, height int) IssueList {
|
||||
issues, err := app.Issues.SearchIssues(context.Background(), "", models.IssueFilter{})
|
||||
if err != nil {
|
||||
return IssueList{}
|
||||
}
|
||||
|
||||
listIssues := []ListIssue{}
|
||||
for _, issue := range issues {
|
||||
listIssues = append(listIssues, ListIssue{Issue: *issue})
|
||||
}
|
||||
|
||||
items := make([]list.Item, len(listIssues))
|
||||
for i, issue := range listIssues {
|
||||
items[i] = issue
|
||||
}
|
||||
|
||||
l := list.New(items, NewIssueListDelegate(width), width, height)
|
||||
l.SetShowTitle(false)
|
||||
l.SetShowHelp(false)
|
||||
l.SetShowStatusBar(false)
|
||||
l.SetFilteringEnabled(true)
|
||||
l.FilterInput.PromptStyle = styles.FilterPromptStyle
|
||||
l.FilterInput.Cursor.Style = styles.FilterStyle
|
||||
l.FilterInput.TextStyle = styles.FilterInputStyle
|
||||
l.FilterInput.Prompt = "🔍 "
|
||||
|
||||
return IssueList{
|
||||
list: l,
|
||||
app: app,
|
||||
width: width,
|
||||
height: height,
|
||||
}
|
||||
}
|
||||
|
||||
func NewIssueListFromIssues(app *app.App, issues []*models.Issue, width, height int) IssueList {
|
||||
// for making an IssueList from a pre-existing list of issues.
|
||||
listIssues := make([]list.Item, len(issues))
|
||||
for i, issue := range issues {
|
||||
listIssues[i] = ListIssue{Issue: *issue}
|
||||
}
|
||||
l := list.New(listIssues, NewIssueListDelegate(width), width, height)
|
||||
l.SetShowTitle(false)
|
||||
l.SetShowHelp(false)
|
||||
l.SetShowStatusBar(false)
|
||||
l.SetFilteringEnabled(true)
|
||||
l.FilterInput.PromptStyle = styles.FilterPromptStyle
|
||||
l.FilterInput.Cursor.Style = styles.FilterStyle
|
||||
l.FilterInput.TextStyle = styles.FilterInputStyle
|
||||
l.FilterInput.Prompt = "🔍 "
|
||||
return IssueList{
|
||||
list: l,
|
||||
app: app,
|
||||
width: width,
|
||||
height: height,
|
||||
}
|
||||
}
|
||||
|
||||
func OpenAndInProgressOnly(issues []*models.Issue) []*models.Issue {
|
||||
// used to display open & in-progress issues in the first window in the dashboard
|
||||
out := make([]*models.Issue, 0, len(issues))
|
||||
for _, issue := range issues {
|
||||
if issue.Status == models.StatusOpen || issue.Status == models.StatusInProgress {
|
||||
out = append(out, issue)
|
||||
}
|
||||
}
|
||||
sortByPriorityDesc(out)
|
||||
return out
|
||||
}
|
||||
|
||||
func ClosedOnly(issues []*models.Issue) []*models.Issue {
|
||||
// used to display issues in the second window in the dashboard
|
||||
out := make([]*models.Issue, 0, len(issues))
|
||||
for _, issue := range issues {
|
||||
if issue.Status == models.StatusClosed {
|
||||
out = append(out, issue)
|
||||
}
|
||||
}
|
||||
sortByPriorityDesc(out)
|
||||
return out
|
||||
}
|
||||
|
||||
func sortByPriorityDesc(issues []*models.Issue) {
|
||||
// sorts issues by priority, highest first.
|
||||
sort.Slice(issues, func(i, j int) bool {
|
||||
return issues[i].Priority > issues[j].Priority
|
||||
})
|
||||
}
|
||||
|
||||
func (l *IssueList) Update(msg tea.Msg) (tea.Cmd, bool) {
|
||||
var cmd tea.Cmd
|
||||
oldIndex := l.list.Index()
|
||||
l.list, cmd = l.list.Update(msg)
|
||||
changed := l.list.Index() != oldIndex
|
||||
return cmd, changed
|
||||
}
|
||||
|
||||
func (l *IssueList) SetSize(width, height int) {
|
||||
l.width = width
|
||||
l.height = height
|
||||
|
||||
l.list.SetSize(width, height)
|
||||
l.list.SetDelegate(NewIssueListDelegate(width))
|
||||
}
|
||||
|
||||
func (l IssueList) View() string {
|
||||
return l.renderResponsive()
|
||||
}
|
||||
|
||||
func (l IssueList) renderResponsive() string {
|
||||
cols := getTableColumns(l.width)
|
||||
header := renderHeaders(cols)
|
||||
|
||||
var content []string
|
||||
|
||||
if l.list.FilterState() == list.Filtering {
|
||||
filterText := l.list.FilterInput.Value()
|
||||
filterView := styles.FilterStyle.Render("🔍 " + filterText)
|
||||
content = append(content, filterView)
|
||||
}
|
||||
|
||||
itemsView := l.renderFilteredItems()
|
||||
content = append(content, header, itemsView)
|
||||
|
||||
return styles.ContainerStyle.
|
||||
Width(l.width).
|
||||
MaxWidth(l.width).
|
||||
MaxHeight(l.height).
|
||||
Render(lipgloss.JoinVertical(lipgloss.Left, content...))
|
||||
}
|
||||
|
||||
func (l IssueList) renderFilteredItems() string {
|
||||
var items []string
|
||||
|
||||
var visibleItems []list.Item
|
||||
if l.list.FilterState() == list.Filtering || l.list.FilterState() == list.FilterApplied {
|
||||
visibleItems = l.list.VisibleItems()
|
||||
} else {
|
||||
visibleItems = l.list.Items()
|
||||
}
|
||||
|
||||
start, end := l.list.Paginator.GetSliceBounds(len(visibleItems))
|
||||
|
||||
cursor := l.list.Index()
|
||||
|
||||
for i := start; i < end && i < len(visibleItems); i++ {
|
||||
isSelected := i == cursor
|
||||
if issue, ok := visibleItems[i].(ListIssue); ok {
|
||||
cols := getTableColumns(l.width)
|
||||
row := renderRow(issue, isSelected, cols)
|
||||
items = append(items, row)
|
||||
}
|
||||
}
|
||||
|
||||
return lipgloss.JoinVertical(lipgloss.Left, items...)
|
||||
}
|
||||
|
||||
func (l IssueList) SelectedItem() ListIssue {
|
||||
if item, ok := l.list.SelectedItem().(ListIssue); ok {
|
||||
return item
|
||||
}
|
||||
return ListIssue{}
|
||||
}
|
||||
|
||||
func (l IssueList) Index() int {
|
||||
return l.list.Index()
|
||||
}
|
||||
|
||||
func (l IssueList) FilterState() list.FilterState {
|
||||
return l.list.FilterState()
|
||||
}
|
||||
|
||||
func (l *IssueList) SetIssues(issues []*models.Issue) tea.Cmd {
|
||||
listIssues := make([]list.Item, len(issues))
|
||||
for i, issue := range issues {
|
||||
listIssues[i] = ListIssue{Issue: *issue}
|
||||
}
|
||||
return l.list.SetItems(listIssues)
|
||||
}
|
||||
|
||||
func (l *IssueList) SelectIssueID(issueID string) {
|
||||
items := l.list.Items()
|
||||
for i := 0; i < len(items); i++ {
|
||||
if item, ok := items[i].(ListIssue); ok && item.ID == issueID {
|
||||
l.list.Select(i)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type IssueListDelegate struct {
|
||||
width int
|
||||
}
|
||||
|
||||
func NewIssueListDelegate(width int) IssueListDelegate {
|
||||
return IssueListDelegate{width: width}
|
||||
}
|
||||
|
||||
func (d IssueListDelegate) Height() int { return 1 }
|
||||
func (d IssueListDelegate) Spacing() int { return 0 }
|
||||
func (d IssueListDelegate) Update(msg tea.Msg, m *list.Model) tea.Cmd { return nil }
|
||||
|
||||
func (d IssueListDelegate) Render(w io.Writer, m list.Model, index int, listItem list.Item) {
|
||||
issue, ok := listItem.(ListIssue)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
isSelected := index == m.Index()
|
||||
cols := getTableColumns(d.width)
|
||||
|
||||
row := renderRow(issue, isSelected, cols)
|
||||
io.WriteString(w, row)
|
||||
}
|
||||
|
||||
func renderRow(issue ListIssue, isSelected bool, cols []TableColumn) string {
|
||||
var parts []string
|
||||
|
||||
for _, col := range cols {
|
||||
value := getColumnValue(col, issue)
|
||||
colWidth := col.width
|
||||
if colWidth == 0 {
|
||||
colWidth = 1
|
||||
}
|
||||
|
||||
style := lipgloss.NewStyle().Width(int(colWidth))
|
||||
if isSelected {
|
||||
style = style.Background(styles.SelectedBackground).Bold(true)
|
||||
}
|
||||
|
||||
truncated := truncate.StringWithTail(value, colWidth, "...")
|
||||
parts = append(parts, style.Render(truncated))
|
||||
}
|
||||
|
||||
return lipgloss.JoinHorizontal(lipgloss.Left, parts...)
|
||||
}
|
||||
|
||||
var priorityCodeNames = map[int]string{
|
||||
0: "irrelevant",
|
||||
1: "low",
|
||||
2: "normal",
|
||||
3: "high",
|
||||
4: "critical",
|
||||
}
|
||||
|
||||
func priorityCodeName(priority int) string {
|
||||
if name, ok := priorityCodeNames[priority]; ok {
|
||||
return name
|
||||
}
|
||||
return fmt.Sprintf("%d", priority)
|
||||
}
|
||||
|
||||
func getColumnValue(col TableColumn, issue ListIssue) string {
|
||||
switch col.key {
|
||||
case "id":
|
||||
return issue.ID
|
||||
case "title":
|
||||
return issue.Title()
|
||||
case "status":
|
||||
return string(issue.Issue.Status)
|
||||
case "type":
|
||||
return string(issue.Issue.IssueType)
|
||||
case "priority":
|
||||
return priorityCodeName(issue.Issue.Priority)
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
143
pkg/tui/views/dashboard2/keys.go
Normal file
143
pkg/tui/views/dashboard2/keys.go
Normal file
@@ -0,0 +1,143 @@
|
||||
package dashboard2
|
||||
|
||||
import (
|
||||
"github.com/LazyBachelor/LazyPM/pkg/tui/msgs"
|
||||
"github.com/charmbracelet/bubbles/key"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
)
|
||||
|
||||
type DashboardKeyMap struct {
|
||||
Help key.Binding
|
||||
Quit key.Binding
|
||||
SelectIssue key.Binding
|
||||
BackToList key.Binding
|
||||
ScrollUp key.Binding
|
||||
ScrollDown key.Binding
|
||||
SwitchWindow key.Binding
|
||||
SwitchToDashboard key.Binding
|
||||
EditTitle key.Binding
|
||||
EditDescription key.Binding
|
||||
ChangeStatus key.Binding
|
||||
ChangePriority key.Binding
|
||||
ChangeType key.Binding
|
||||
AddIssue key.Binding
|
||||
DeleteIssue key.Binding
|
||||
}
|
||||
|
||||
var defaultDashboardKeyMap = DashboardKeyMap{
|
||||
Help: key.NewBinding(
|
||||
key.WithKeys("?"),
|
||||
key.WithHelp("?", "help"),
|
||||
),
|
||||
Quit: key.NewBinding(
|
||||
key.WithKeys("q", "ctrl+c"),
|
||||
key.WithHelp("q", "quit"),
|
||||
),
|
||||
SelectIssue: key.NewBinding(
|
||||
key.WithKeys("enter"),
|
||||
key.WithHelp("enter", "view issue"),
|
||||
),
|
||||
BackToList: key.NewBinding(
|
||||
key.WithKeys("b"),
|
||||
key.WithHelp("b", "back to list"),
|
||||
),
|
||||
ScrollUp: key.NewBinding(
|
||||
key.WithKeys("up", "k"),
|
||||
key.WithHelp("↑/k", "up"),
|
||||
),
|
||||
ScrollDown: key.NewBinding(
|
||||
key.WithKeys("down", "j"),
|
||||
key.WithHelp("↓/j", "down"),
|
||||
),
|
||||
SwitchWindow: key.NewBinding(
|
||||
key.WithKeys("tab"),
|
||||
key.WithHelp("tab", "switch window"),
|
||||
),
|
||||
SwitchToDashboard: key.NewBinding(
|
||||
key.WithKeys("1"),
|
||||
key.WithHelp("1", "dashboard 1"),
|
||||
),
|
||||
EditTitle: key.NewBinding(
|
||||
key.WithKeys("e"),
|
||||
key.WithHelp("e", "edit title"),
|
||||
),
|
||||
EditDescription: key.NewBinding(
|
||||
key.WithKeys("d"),
|
||||
key.WithHelp("d", "edit description"),
|
||||
),
|
||||
ChangeStatus: key.NewBinding(
|
||||
key.WithKeys("s"),
|
||||
key.WithHelp("s", "change status"),
|
||||
),
|
||||
ChangePriority: key.NewBinding(
|
||||
key.WithKeys("p"),
|
||||
key.WithHelp("p", "change priority"),
|
||||
),
|
||||
ChangeType: key.NewBinding(
|
||||
key.WithKeys("t"),
|
||||
key.WithHelp("t", "change type"),
|
||||
),
|
||||
AddIssue: key.NewBinding(
|
||||
key.WithKeys("a"),
|
||||
key.WithHelp("a", "add issue"),
|
||||
),
|
||||
DeleteIssue: key.NewBinding(
|
||||
key.WithKeys("x"),
|
||||
key.WithHelp("x", "delete issue"),
|
||||
),
|
||||
}
|
||||
|
||||
func (d *Model) handleKeyMsg(msg tea.KeyMsg) tea.Cmd {
|
||||
var cmd tea.Cmd
|
||||
|
||||
switch {
|
||||
case key.Matches(msg, d.keyMap.Help):
|
||||
d.helpBar.ToggleHelp()
|
||||
case key.Matches(msg, d.keyMap.Quit):
|
||||
return tea.Quit
|
||||
case key.Matches(msg, d.keyMap.SwitchWindow):
|
||||
d.ToggleFocusedWindow()
|
||||
case !d.editingTitle && !d.creatingIssue && !d.editingDescription && !d.choosingStatus && !d.choosingPriority && !d.confirmingDelete && !d.choosingType && key.Matches(msg, d.keyMap.SwitchToDashboard):
|
||||
return func() tea.Msg { return msgs.SwitchToDashboardMsg{} }
|
||||
case d.IsFocusedOnList() && key.Matches(msg, d.keyMap.SelectIssue):
|
||||
d.FocusDetail()
|
||||
case d.IsFocusedOnDetail() && key.Matches(msg, d.keyMap.BackToList):
|
||||
d.FocusList()
|
||||
case d.IsFocusedOnDetail() && key.Matches(msg, d.keyMap.ScrollUp):
|
||||
d.issueDetail.ScrollUp(1)
|
||||
case d.IsFocusedOnDetail() && key.Matches(msg, d.keyMap.ScrollDown):
|
||||
d.issueDetail.ScrollDown(1)
|
||||
case !d.editingTitle && !d.creatingIssue && !d.editingDescription && !d.choosingStatus && !d.choosingPriority && !d.confirmingDelete && !d.choosingType && key.Matches(msg, d.keyMap.EditTitle):
|
||||
if selected := d.FocusedIssueList().SelectedItem(); selected.ID != "" {
|
||||
d.startEditTitle(selected)
|
||||
cmd = d.titleInput.Focus()
|
||||
}
|
||||
case !d.editingTitle && !d.creatingIssue && !d.editingDescription && !d.choosingStatus && !d.choosingPriority && !d.confirmingDelete && !d.choosingType && key.Matches(msg, d.keyMap.EditDescription):
|
||||
if selected := d.FocusedIssueList().SelectedItem(); selected.ID != "" {
|
||||
d.startEditDescription(selected)
|
||||
cmd = d.descriptionInput.Focus()
|
||||
}
|
||||
case !d.editingTitle && !d.creatingIssue && !d.editingDescription && !d.choosingStatus && !d.choosingPriority && !d.confirmingDelete && !d.choosingType && key.Matches(msg, d.keyMap.ChangeStatus):
|
||||
if selected := d.FocusedIssueList().SelectedItem(); selected.ID != "" {
|
||||
d.startChooseStatus(selected)
|
||||
}
|
||||
case !d.editingTitle && !d.creatingIssue && !d.editingDescription && !d.choosingStatus && !d.choosingPriority && !d.confirmingDelete && !d.choosingType && key.Matches(msg, d.keyMap.ChangePriority):
|
||||
if selected := d.FocusedIssueList().SelectedItem(); selected.ID != "" {
|
||||
d.startChoosePriority(selected)
|
||||
}
|
||||
case !d.editingTitle && !d.creatingIssue && !d.editingDescription && !d.choosingStatus && !d.choosingPriority && !d.confirmingDelete && !d.choosingType && key.Matches(msg, d.keyMap.ChangeType):
|
||||
if selected := d.FocusedIssueList().SelectedItem(); selected.ID != "" {
|
||||
d.startChooseType(selected)
|
||||
}
|
||||
case !d.editingTitle && !d.creatingIssue && !d.editingDescription && !d.choosingStatus && !d.choosingPriority && !d.confirmingDelete && !d.choosingType && key.Matches(msg, d.keyMap.AddIssue):
|
||||
d.startCreateIssue()
|
||||
cmd = d.createTitleInput.Focus()
|
||||
case !d.editingTitle && !d.creatingIssue && !d.editingDescription && !d.choosingStatus && !d.choosingPriority && !d.confirmingDelete && !d.choosingType && key.Matches(msg, d.keyMap.DeleteIssue):
|
||||
fl := d.FocusedIssueList()
|
||||
if selected := fl.SelectedItem(); selected.ID != "" {
|
||||
d.startConfirmDelete(selected.ID, fl.Index())
|
||||
}
|
||||
}
|
||||
|
||||
return cmd
|
||||
}
|
||||
214
pkg/tui/views/dashboard2/model.go
Normal file
214
pkg/tui/views/dashboard2/model.go
Normal file
@@ -0,0 +1,214 @@
|
||||
package dashboard2
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/LazyBachelor/LazyPM/internal/app"
|
||||
"github.com/LazyBachelor/LazyPM/internal/models"
|
||||
"github.com/charmbracelet/bubbles/textarea"
|
||||
"github.com/charmbracelet/bubbles/textinput"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
)
|
||||
|
||||
type ValidationFeedbackMsg struct {
|
||||
Feedback models.ValidationFeedback
|
||||
}
|
||||
|
||||
type Model struct {
|
||||
header Header
|
||||
issueList IssueList
|
||||
issueDetail IssueDetail
|
||||
closedIssueList IssueList
|
||||
helpBar HelpBar
|
||||
keyMap DashboardKeyMap
|
||||
app *app.App
|
||||
width int
|
||||
height int
|
||||
focusedWindow int // 0 = main (display issues), 1 = closed issues
|
||||
focusedPaneMain int // 0 = list, 1 = detail
|
||||
focusedPaneClosed int
|
||||
editingTitle bool // true while we are editing a title
|
||||
titleInput textinput.Model
|
||||
editingIssueID string
|
||||
|
||||
editingDescription bool // true while editing a description
|
||||
descriptionInput textarea.Model
|
||||
editingDescIssueID string
|
||||
creatingIssue bool // true while creating a new issue
|
||||
createTitleInput textinput.Model
|
||||
|
||||
confirmingDelete bool // true while confirming a delete
|
||||
deleteConfirmID string
|
||||
deleteConfirmIndex int
|
||||
|
||||
choosingStatus bool // true while choosing a status
|
||||
statusIssueID string
|
||||
choosingPriority bool // true while choosing a priority
|
||||
priorityIssueID string
|
||||
choosingType bool // true while choosing a type
|
||||
typeIssueID string
|
||||
feedbackChan chan models.ValidationFeedback
|
||||
quitChan chan bool
|
||||
currentFeedback models.ValidationFeedback
|
||||
showComplete bool
|
||||
}
|
||||
|
||||
func NewDashboard(app *app.App, feedbackChan chan models.ValidationFeedback, quitChan chan bool) *Model {
|
||||
m := &Model{
|
||||
header: NewHeader("Project Manager Dashboard"),
|
||||
keyMap: defaultDashboardKeyMap,
|
||||
app: app,
|
||||
width: 80,
|
||||
height: 24,
|
||||
focusedWindow: 0,
|
||||
focusedPaneMain: 0,
|
||||
focusedPaneClosed: 0,
|
||||
feedbackChan: feedbackChan,
|
||||
quitChan: quitChan,
|
||||
}
|
||||
|
||||
allIssues, _ := app.Issues.SearchIssues(context.Background(), "", models.IssueFilter{})
|
||||
m.issueList = NewIssueListFromIssues(app, OpenAndInProgressOnly(allIssues), 0, 0)
|
||||
m.issueDetail = NewIssueDetail()
|
||||
m.closedIssueList = NewIssueListFromIssues(app, ClosedOnly(allIssues), 0, 0)
|
||||
m.helpBar = NewHelpBar(m.keyMap)
|
||||
|
||||
ti := textinput.New()
|
||||
ti.Placeholder = "Issue title ..."
|
||||
ti.CharLimit = 256
|
||||
m.titleInput = ti
|
||||
|
||||
createTi := textinput.New()
|
||||
createTi.Placeholder = "New issue title ..."
|
||||
createTi.CharLimit = 256
|
||||
m.createTitleInput = createTi
|
||||
|
||||
descTa := textarea.New()
|
||||
descTa.Placeholder = "Issue description..."
|
||||
descTa.SetWidth(56)
|
||||
descTa.SetHeight(8)
|
||||
m.descriptionInput = descTa
|
||||
|
||||
if selected := m.issueList.SelectedItem(); selected.ID != "" {
|
||||
m.issueDetail.SetIssue(selected.Issue)
|
||||
} else if selected := m.closedIssueList.SelectedItem(); selected.ID != "" {
|
||||
m.issueDetail.SetIssue(selected.Issue)
|
||||
}
|
||||
|
||||
return m
|
||||
}
|
||||
|
||||
func (m *Model) startEditTitle(selected ListIssue) {
|
||||
m.editingTitle = true
|
||||
m.editingIssueID = selected.ID
|
||||
m.titleInput.SetValue(selected.Issue.Title)
|
||||
m.titleInput.CursorEnd()
|
||||
}
|
||||
|
||||
func (m *Model) startEditDescription(selected ListIssue) {
|
||||
m.editingDescription = true
|
||||
m.editingDescIssueID = selected.ID
|
||||
m.descriptionInput.SetValue(selected.Issue.Description)
|
||||
m.descriptionInput.CursorEnd()
|
||||
}
|
||||
|
||||
func (m *Model) startCreateIssue() {
|
||||
m.creatingIssue = true
|
||||
m.createTitleInput.SetValue("")
|
||||
m.createTitleInput.Reset()
|
||||
}
|
||||
|
||||
func (m *Model) startConfirmDelete(issueID string, index int) {
|
||||
m.confirmingDelete = true
|
||||
m.deleteConfirmID = issueID
|
||||
m.deleteConfirmIndex = index
|
||||
}
|
||||
|
||||
func (m *Model) startChooseStatus(selected ListIssue) {
|
||||
m.choosingStatus = true
|
||||
m.statusIssueID = selected.ID
|
||||
}
|
||||
|
||||
func (m *Model) startChoosePriority(selected ListIssue) {
|
||||
m.choosingPriority = true
|
||||
m.priorityIssueID = selected.ID
|
||||
}
|
||||
|
||||
func (m *Model) startChooseType(selected ListIssue) {
|
||||
m.choosingType = true
|
||||
m.typeIssueID = selected.ID
|
||||
}
|
||||
|
||||
func (m *Model) Init() tea.Cmd {
|
||||
return m.listenForValidation()
|
||||
}
|
||||
|
||||
func (m *Model) listenForValidation() tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
feedback := <-m.feedbackChan
|
||||
return ValidationFeedbackMsg{Feedback: feedback}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Model) IsFocusedOnList() bool {
|
||||
if m.focusedWindow == 0 {
|
||||
return m.focusedPaneMain == 0
|
||||
}
|
||||
return m.focusedPaneClosed == 0
|
||||
}
|
||||
|
||||
func (m *Model) IsFocusedOnDetail() bool {
|
||||
if m.focusedWindow == 0 {
|
||||
return m.focusedPaneMain == 1
|
||||
}
|
||||
return m.focusedPaneClosed == 1
|
||||
}
|
||||
|
||||
func (m *Model) FocusList() {
|
||||
if m.focusedWindow == 0 {
|
||||
m.focusedPaneMain = 0
|
||||
} else {
|
||||
m.focusedPaneClosed = 0
|
||||
}
|
||||
m.issueDetail.SetFocused(false)
|
||||
}
|
||||
|
||||
func (m *Model) FocusDetail() {
|
||||
if m.focusedWindow == 0 {
|
||||
m.focusedPaneMain = 1
|
||||
} else {
|
||||
m.focusedPaneClosed = 1
|
||||
}
|
||||
m.issueDetail.SetFocused(true)
|
||||
}
|
||||
|
||||
func (m *Model) ToggleFocus() {
|
||||
if m.IsFocusedOnList() {
|
||||
m.FocusDetail()
|
||||
} else {
|
||||
m.FocusList()
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Model) FocusedIssueList() *IssueList {
|
||||
// return the issue list of the currently focused window so we can use two tui windows for issues
|
||||
if m.focusedWindow == 0 {
|
||||
return &m.issueList
|
||||
}
|
||||
return &m.closedIssueList
|
||||
}
|
||||
|
||||
func (m *Model) ToggleFocusedWindow() {
|
||||
// switch focus between open/in-progress and closed issues window
|
||||
m.focusedWindow = 1 - m.focusedWindow
|
||||
if m.focusedWindow == 0 {
|
||||
if selected := m.issueList.SelectedItem(); selected.ID != "" {
|
||||
m.issueDetail.SetIssue(selected.Issue)
|
||||
}
|
||||
} else {
|
||||
if selected := m.closedIssueList.SelectedItem(); selected.ID != "" {
|
||||
m.issueDetail.SetIssue(selected.Issue)
|
||||
}
|
||||
}
|
||||
m.issueDetail.SetFocused(m.IsFocusedOnDetail())
|
||||
}
|
||||
456
pkg/tui/views/dashboard2/operations.go
Normal file
456
pkg/tui/views/dashboard2/operations.go
Normal file
@@ -0,0 +1,456 @@
|
||||
package dashboard2
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/LazyBachelor/LazyPM/internal/app"
|
||||
"github.com/LazyBachelor/LazyPM/internal/models"
|
||||
"github.com/charmbracelet/bubbles/list"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
)
|
||||
|
||||
type issueTitleUpdatedMsg struct {
|
||||
IssueID string
|
||||
Err error
|
||||
}
|
||||
|
||||
type issueDescriptionUpdatedMsg struct {
|
||||
IssueID string
|
||||
Err error
|
||||
}
|
||||
|
||||
type issueStatusUpdatedMsg struct {
|
||||
IssueID string
|
||||
Err error
|
||||
}
|
||||
|
||||
type issuePriorityUpdatedMsg struct {
|
||||
IssueID string
|
||||
Err error
|
||||
}
|
||||
|
||||
type issueTypeUpdatedMsg struct {
|
||||
IssueID string
|
||||
Err error
|
||||
}
|
||||
|
||||
type selectIssueMsg struct {
|
||||
IssueID string
|
||||
}
|
||||
|
||||
type issueCreatedMsg struct {
|
||||
Issue *models.Issue
|
||||
Err error
|
||||
}
|
||||
|
||||
type issueDeletedMsg struct {
|
||||
IssueID string
|
||||
Err error
|
||||
PreviousIndex int
|
||||
}
|
||||
|
||||
func updateIssueTitleCmd(app *app.App, issueID, newTitle string) tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
updates := map[string]interface{}{"title": newTitle}
|
||||
err := app.Issues.UpdateIssue(context.Background(), issueID, updates, "tui")
|
||||
return issueTitleUpdatedMsg{IssueID: issueID, Err: err}
|
||||
}
|
||||
}
|
||||
|
||||
func updateIssueDescriptionCmd(app *app.App, issueID, newDescription string) tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
updates := map[string]interface{}{"description": newDescription}
|
||||
err := app.Issues.UpdateIssue(context.Background(), issueID, updates, "tui")
|
||||
return issueDescriptionUpdatedMsg{IssueID: issueID, Err: err}
|
||||
}
|
||||
}
|
||||
|
||||
func updateIssueStatusCmd(app *app.App, issueID, status string) tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
updates := map[string]interface{}{"status": status}
|
||||
err := app.Issues.UpdateIssue(context.Background(), issueID, updates, "tui")
|
||||
return issueStatusUpdatedMsg{IssueID: issueID, Err: err}
|
||||
}
|
||||
}
|
||||
|
||||
func updateIssuePriorityCmd(app *app.App, issueID string, priority int) tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
updates := map[string]interface{}{"priority": priority}
|
||||
err := app.Issues.UpdateIssue(context.Background(), issueID, updates, "tui")
|
||||
return issuePriorityUpdatedMsg{IssueID: issueID, Err: err}
|
||||
}
|
||||
}
|
||||
|
||||
func updateIssueTypeCmd(app *app.App, issueID string, issueType models.IssueType) tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
updates := map[string]interface{}{"issue_type": string(issueType)}
|
||||
err := app.Issues.UpdateIssue(context.Background(), issueID, updates, "tui")
|
||||
return issueTypeUpdatedMsg{IssueID: issueID, Err: err}
|
||||
}
|
||||
}
|
||||
|
||||
func createIssueCmd(app *app.App, title string) tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
issue := &models.Issue{
|
||||
Title: title,
|
||||
Status: models.StatusOpen,
|
||||
IssueType: models.TypeTask,
|
||||
Priority: 2,
|
||||
}
|
||||
err := app.Issues.CreateIssue(context.Background(), issue, "tui")
|
||||
return issueCreatedMsg{Issue: issue, Err: err}
|
||||
}
|
||||
}
|
||||
|
||||
func deleteIssueCmd(app *app.App, issueID string, currentIndex int) tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
err := app.Issues.DeleteIssue(context.Background(), issueID)
|
||||
return issueDeletedMsg{IssueID: issueID, Err: err, PreviousIndex: currentIndex}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Model) refreshIssueListsAndSelectIssue(issueID string) tea.Cmd {
|
||||
/* update handler for issueTitleUpdatedMsg, issueDescriptionUpdatedMsg, and issueStatusUpdatedMsg to avoid using nearly identical code for refreshing the issue lists and updating the detail view
|
||||
Fetch all issues, update both lists, set the detail view for the given issue, and return a command to select that issue. Returns nil if fetch fails.
|
||||
*/
|
||||
issues, err := m.app.Issues.SearchIssues(context.Background(), "", models.IssueFilter{})
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
setItemsCmd := m.issueList.SetIssues(OpenAndInProgressOnly(issues))
|
||||
closedSetCmd := m.closedIssueList.SetIssues(ClosedOnly(issues))
|
||||
for _, issue := range issues {
|
||||
if issue.ID == issueID {
|
||||
m.issueDetail.SetIssue(*issue)
|
||||
break
|
||||
}
|
||||
}
|
||||
return tea.Sequence(setItemsCmd, closedSetCmd, func() tea.Msg { return selectIssueMsg{IssueID: issueID} })
|
||||
}
|
||||
|
||||
func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
switch msg := msg.(type) {
|
||||
case issueTitleUpdatedMsg:
|
||||
m.editingTitle = false
|
||||
m.editingIssueID = ""
|
||||
m.titleInput.Blur()
|
||||
if msg.Err != nil {
|
||||
return m, nil
|
||||
}
|
||||
return m, m.refreshIssueListsAndSelectIssue(msg.IssueID)
|
||||
|
||||
case issueDescriptionUpdatedMsg:
|
||||
m.editingDescription = false
|
||||
m.editingDescIssueID = ""
|
||||
m.descriptionInput.Blur()
|
||||
if msg.Err != nil {
|
||||
return m, nil
|
||||
}
|
||||
return m, m.refreshIssueListsAndSelectIssue(msg.IssueID)
|
||||
|
||||
case issueStatusUpdatedMsg:
|
||||
m.choosingStatus = false
|
||||
m.statusIssueID = ""
|
||||
if msg.Err != nil {
|
||||
return m, nil
|
||||
}
|
||||
return m, m.refreshIssueListsAndSelectIssue(msg.IssueID)
|
||||
|
||||
case issuePriorityUpdatedMsg:
|
||||
m.choosingPriority = false
|
||||
m.priorityIssueID = ""
|
||||
if msg.Err != nil {
|
||||
return m, nil
|
||||
}
|
||||
return m, m.refreshIssueListsAndSelectIssue(msg.IssueID)
|
||||
|
||||
case issueTypeUpdatedMsg:
|
||||
m.choosingType = false
|
||||
m.typeIssueID = ""
|
||||
if msg.Err != nil {
|
||||
return m, nil
|
||||
}
|
||||
return m, m.refreshIssueListsAndSelectIssue(msg.IssueID)
|
||||
|
||||
case selectIssueMsg:
|
||||
m.issueList.SelectIssueID(msg.IssueID)
|
||||
m.closedIssueList.SelectIssueID(msg.IssueID)
|
||||
return m, nil
|
||||
|
||||
case issueCreatedMsg:
|
||||
m.creatingIssue = false
|
||||
m.createTitleInput.Blur()
|
||||
m.createTitleInput.Reset()
|
||||
if msg.Err != nil || msg.Issue == nil {
|
||||
return m, nil
|
||||
}
|
||||
issues, err := m.app.Issues.SearchIssues(context.Background(), "", models.IssueFilter{})
|
||||
if err != nil {
|
||||
return m, nil
|
||||
}
|
||||
setItemsCmd := m.issueList.SetIssues(OpenAndInProgressOnly(issues))
|
||||
closedSetCmd := m.closedIssueList.SetIssues(ClosedOnly(issues))
|
||||
|
||||
// Determine the created issue from the refreshed list to ensure all fields (like ID) are populated.
|
||||
selectedIssue := msg.Issue
|
||||
if selectedIssue.ID == "" {
|
||||
for _, issue := range issues {
|
||||
// Prefer an issue that matches the created issue's title when ID is not yet known.
|
||||
if issue.Title == msg.Issue.Title {
|
||||
selectedIssue = issue
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
m.issueDetail.SetIssue(*selectedIssue)
|
||||
return m, tea.Sequence(setItemsCmd, closedSetCmd, func() tea.Msg { return selectIssueMsg{IssueID: selectedIssue.ID} })
|
||||
case issueDeletedMsg:
|
||||
m.confirmingDelete = false
|
||||
m.deleteConfirmID = ""
|
||||
if msg.Err != nil {
|
||||
return m, nil
|
||||
}
|
||||
issues, err := m.app.Issues.SearchIssues(context.Background(), "", models.IssueFilter{})
|
||||
if err != nil {
|
||||
return m, nil
|
||||
}
|
||||
openIssues := OpenAndInProgressOnly(issues)
|
||||
closedIssues := ClosedOnly(issues)
|
||||
setItemsCmd := m.issueList.SetIssues(openIssues)
|
||||
closedSetCmd := m.closedIssueList.SetIssues(closedIssues)
|
||||
// If there are no issues at all, clear the detail view and return.
|
||||
if len(openIssues) == 0 && len(closedIssues) == 0 {
|
||||
m.issueDetail.SetIssue(models.Issue{})
|
||||
return m, tea.Sequence(setItemsCmd, closedSetCmd)
|
||||
}
|
||||
|
||||
// Determine which list to use for the next selection.
|
||||
var targetIssues []*models.Issue
|
||||
if m.focusedWindow == 0 {
|
||||
targetIssues = openIssues
|
||||
if len(targetIssues) == 0 && len(closedIssues) > 0 {
|
||||
// The open list became empty; fall back to closed issues.
|
||||
targetIssues = closedIssues
|
||||
m.focusedWindow = 1
|
||||
}
|
||||
} else {
|
||||
targetIssues = closedIssues
|
||||
if len(targetIssues) == 0 && len(openIssues) > 0 {
|
||||
// The closed list became empty; fall back to open/in-progress issues.
|
||||
targetIssues = openIssues
|
||||
m.focusedWindow = 0
|
||||
}
|
||||
}
|
||||
|
||||
// Safety: if targetIssues is still empty here, just clear detail and return.
|
||||
if len(targetIssues) == 0 {
|
||||
m.issueDetail.SetIssue(models.Issue{})
|
||||
return m, tea.Sequence(setItemsCmd, closedSetCmd)
|
||||
}
|
||||
|
||||
newIndex := msg.PreviousIndex
|
||||
if newIndex >= len(targetIssues) {
|
||||
newIndex = len(targetIssues) - 1
|
||||
}
|
||||
selectedIssue := targetIssues[newIndex]
|
||||
m.issueDetail.SetIssue(*selectedIssue)
|
||||
return m, tea.Sequence(setItemsCmd, closedSetCmd, func() tea.Msg {
|
||||
return selectIssueMsg{IssueID: selectedIssue.ID}
|
||||
})
|
||||
|
||||
case tea.KeyMsg:
|
||||
if m.confirmingDelete {
|
||||
switch msg.String() {
|
||||
case "y", "Y":
|
||||
issueID := m.deleteConfirmID
|
||||
idx := m.deleteConfirmIndex
|
||||
m.confirmingDelete = false
|
||||
m.deleteConfirmID = ""
|
||||
return m, deleteIssueCmd(m.app, issueID, idx)
|
||||
case "n", "N", "esc":
|
||||
m.confirmingDelete = false
|
||||
m.deleteConfirmID = ""
|
||||
return m, nil
|
||||
}
|
||||
}
|
||||
|
||||
if m.choosingStatus {
|
||||
switch msg.String() {
|
||||
case "o":
|
||||
issueID := m.statusIssueID
|
||||
m.choosingStatus = false
|
||||
m.statusIssueID = ""
|
||||
return m, updateIssueStatusCmd(m.app, issueID, string(models.StatusOpen))
|
||||
case "i":
|
||||
issueID := m.statusIssueID
|
||||
m.choosingStatus = false
|
||||
m.statusIssueID = ""
|
||||
return m, updateIssueStatusCmd(m.app, issueID, string(models.StatusInProgress))
|
||||
case "c":
|
||||
issueID := m.statusIssueID
|
||||
m.choosingStatus = false
|
||||
m.statusIssueID = ""
|
||||
return m, updateIssueStatusCmd(m.app, issueID, string(models.StatusClosed))
|
||||
case "esc":
|
||||
m.choosingStatus = false
|
||||
m.statusIssueID = ""
|
||||
return m, nil
|
||||
}
|
||||
}
|
||||
|
||||
if m.choosingPriority {
|
||||
switch msg.String() {
|
||||
case "0", "1", "2", "3", "4":
|
||||
issueID := m.priorityIssueID
|
||||
priority := int(msg.String()[0] - '0')
|
||||
m.choosingPriority = false
|
||||
m.priorityIssueID = ""
|
||||
return m, updateIssuePriorityCmd(m.app, issueID, priority)
|
||||
case "esc":
|
||||
m.choosingPriority = false
|
||||
m.priorityIssueID = ""
|
||||
return m, nil
|
||||
default:
|
||||
return m, nil
|
||||
}
|
||||
}
|
||||
|
||||
if m.choosingType {
|
||||
switch msg.String() {
|
||||
case "b":
|
||||
issueID := m.typeIssueID
|
||||
m.choosingType = false
|
||||
m.typeIssueID = ""
|
||||
return m, updateIssueTypeCmd(m.app, issueID, models.TypeBug)
|
||||
case "f":
|
||||
issueID := m.typeIssueID
|
||||
m.choosingType = false
|
||||
m.typeIssueID = ""
|
||||
return m, updateIssueTypeCmd(m.app, issueID, models.TypeFeature)
|
||||
case "t":
|
||||
issueID := m.typeIssueID
|
||||
m.choosingType = false
|
||||
m.typeIssueID = ""
|
||||
return m, updateIssueTypeCmd(m.app, issueID, models.TypeTask)
|
||||
case "e":
|
||||
issueID := m.typeIssueID
|
||||
m.choosingType = false
|
||||
m.typeIssueID = ""
|
||||
return m, updateIssueTypeCmd(m.app, issueID, models.TypeEpic)
|
||||
case "c":
|
||||
issueID := m.typeIssueID
|
||||
m.choosingType = false
|
||||
m.typeIssueID = ""
|
||||
return m, updateIssueTypeCmd(m.app, issueID, models.TypeChore)
|
||||
case "esc":
|
||||
m.choosingType = false
|
||||
m.typeIssueID = ""
|
||||
return m, nil
|
||||
default:
|
||||
return m, nil
|
||||
}
|
||||
}
|
||||
|
||||
if m.creatingIssue {
|
||||
if msg.String() == "enter" {
|
||||
title := m.createTitleInput.Value()
|
||||
if title != "" {
|
||||
return m, createIssueCmd(m.app, title)
|
||||
}
|
||||
}
|
||||
if msg.String() == "esc" {
|
||||
m.creatingIssue = false
|
||||
m.createTitleInput.Blur()
|
||||
m.createTitleInput.Reset()
|
||||
return m, nil
|
||||
}
|
||||
var cmd tea.Cmd
|
||||
m.createTitleInput, cmd = m.createTitleInput.Update(msg)
|
||||
return m, cmd
|
||||
}
|
||||
|
||||
if m.editingTitle {
|
||||
if msg.String() == "enter" {
|
||||
newTitle := m.titleInput.Value()
|
||||
if newTitle != "" {
|
||||
return m, updateIssueTitleCmd(m.app, m.editingIssueID, newTitle)
|
||||
}
|
||||
}
|
||||
if msg.String() == "esc" {
|
||||
m.editingTitle = false
|
||||
m.editingIssueID = ""
|
||||
m.titleInput.Blur()
|
||||
return m, nil
|
||||
}
|
||||
var cmd tea.Cmd
|
||||
m.titleInput, cmd = m.titleInput.Update(msg)
|
||||
return m, cmd
|
||||
}
|
||||
|
||||
if m.editingDescription {
|
||||
if msg.String() == "ctrl+s" {
|
||||
issueID := m.editingDescIssueID
|
||||
newDesc := m.descriptionInput.Value()
|
||||
m.editingDescription = false
|
||||
m.editingDescIssueID = ""
|
||||
m.descriptionInput.Blur()
|
||||
return m, updateIssueDescriptionCmd(m.app, issueID, newDesc)
|
||||
}
|
||||
if msg.String() == "esc" {
|
||||
m.editingDescription = false
|
||||
m.editingDescIssueID = ""
|
||||
m.descriptionInput.Blur()
|
||||
return m, nil
|
||||
}
|
||||
var cmd tea.Cmd
|
||||
m.descriptionInput, cmd = m.descriptionInput.Update(msg)
|
||||
return m, cmd
|
||||
}
|
||||
|
||||
focusedList := m.FocusedIssueList()
|
||||
if focusedList.FilterState() == list.Filtering {
|
||||
cmd, _ := focusedList.Update(msg)
|
||||
return m, cmd
|
||||
}
|
||||
|
||||
// On main dashboard, ESC does nothing; only q quits; like in lazybeads.
|
||||
if msg.String() == "esc" {
|
||||
return m, nil
|
||||
}
|
||||
|
||||
cmd := m.handleKeyMsg(msg)
|
||||
if cmd != nil {
|
||||
return m, cmd
|
||||
}
|
||||
case ValidationFeedbackMsg:
|
||||
m.currentFeedback = msg.Feedback
|
||||
if msg.Feedback.Success {
|
||||
m.showComplete = true
|
||||
return m, tea.Quit
|
||||
}
|
||||
return m, m.listenForValidation()
|
||||
|
||||
case tea.WindowSizeMsg:
|
||||
m.width = msg.Width
|
||||
m.height = msg.Height
|
||||
return m, nil
|
||||
}
|
||||
|
||||
if m.focusedWindow == 0 {
|
||||
cmd, changed := m.issueList.Update(msg)
|
||||
if changed {
|
||||
if selected := m.issueList.SelectedItem(); selected.ID != "" {
|
||||
m.issueDetail.SetIssue(selected.Issue)
|
||||
}
|
||||
}
|
||||
return m, cmd
|
||||
}
|
||||
cmd, changed := m.closedIssueList.Update(msg)
|
||||
if changed {
|
||||
if selected := m.closedIssueList.SelectedItem(); selected.ID != "" {
|
||||
m.issueDetail.SetIssue(selected.Issue)
|
||||
}
|
||||
}
|
||||
return m, cmd
|
||||
}
|
||||
170
pkg/tui/views/dashboard2/view.go
Normal file
170
pkg/tui/views/dashboard2/view.go
Normal file
@@ -0,0 +1,170 @@
|
||||
package dashboard2
|
||||
|
||||
import (
|
||||
"github.com/LazyBachelor/LazyPM/pkg/tui/styles"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
)
|
||||
|
||||
func (m *Model) View() string {
|
||||
if m.width == 0 || m.height == 0 {
|
||||
// if there is no space just print a loading message
|
||||
return "Loading..."
|
||||
}
|
||||
|
||||
m.helpBar.SetWidth(m.width)
|
||||
|
||||
header := m.header.View(m.width)
|
||||
headerHeight := m.header.Height()
|
||||
|
||||
footer := m.footer()
|
||||
footerHeight := lipgloss.Height(footer)
|
||||
|
||||
// To avoid layer overflow or clipping, the label heights are calculated and subtracted from the available height before calculating the list heights to avoid layout overflow or clipping.
|
||||
contentHeight := m.height - headerHeight - footerHeight
|
||||
|
||||
mainLabel := styles.LabelStyle.Render("Display issues")
|
||||
closedLabel := styles.LabelStyle.Render("Closed issues")
|
||||
labelHeight := lipgloss.Height(mainLabel) + lipgloss.Height(closedLabel)
|
||||
availableForLists := contentHeight - labelHeight
|
||||
halfHeight := availableForLists / 2
|
||||
if halfHeight < 1 {
|
||||
halfHeight = 1
|
||||
}
|
||||
|
||||
totalContentWidth := m.width - 1
|
||||
listWidth := totalContentWidth * styles.ListViewRatio / 100
|
||||
detailWidth := totalContentWidth - listWidth
|
||||
|
||||
m.issueList.SetSize(listWidth, halfHeight)
|
||||
m.closedIssueList.SetSize(listWidth, halfHeight)
|
||||
m.issueDetail.SetSize(detailWidth, contentHeight)
|
||||
|
||||
listView := m.issueList.View()
|
||||
closedListView := m.closedIssueList.View()
|
||||
detailView := m.issueDetail.View()
|
||||
|
||||
if m.focusedWindow == 0 {
|
||||
mainLabel = lipgloss.NewStyle().Foreground(styles.Primary).Bold(true).Render("Display issues ▶")
|
||||
} else {
|
||||
closedLabel = lipgloss.NewStyle().Foreground(styles.Primary).Bold(true).Render("Closed issues ▶")
|
||||
}
|
||||
leftColumn := lipgloss.JoinVertical(lipgloss.Left,
|
||||
mainLabel, listView,
|
||||
closedLabel, closedListView,
|
||||
)
|
||||
content := lipgloss.JoinHorizontal(lipgloss.Left, leftColumn, detailView)
|
||||
|
||||
mainView := lipgloss.JoinVertical(lipgloss.Left, header, content, footer)
|
||||
|
||||
if m.editingTitle {
|
||||
editBoxWidth := min(60, m.width-4)
|
||||
m.titleInput.Width = editBoxWidth - 2
|
||||
editContent := lipgloss.JoinVertical(lipgloss.Left,
|
||||
styles.LabelStyle.Render("Edit title (Enter to save, Esc to cancel):"),
|
||||
m.titleInput.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.editingDescription {
|
||||
editBoxWidth := min(60, m.width-4)
|
||||
m.descriptionInput.SetWidth(editBoxWidth - 2)
|
||||
m.descriptionInput.SetHeight(10)
|
||||
editContent := lipgloss.JoinVertical(lipgloss.Left,
|
||||
styles.LabelStyle.Render("Edit description (Ctrl+S to save, Esc to cancel):"),
|
||||
m.descriptionInput.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.creatingIssue {
|
||||
createBoxWidth := min(60, m.width-4)
|
||||
m.createTitleInput.Width = createBoxWidth - 2
|
||||
createContent := lipgloss.JoinVertical(lipgloss.Left,
|
||||
styles.LabelStyle.Render("New issue (Enter to create, Esc to cancel):"),
|
||||
m.createTitleInput.View(),
|
||||
)
|
||||
createBox := styles.ContainerStyle.
|
||||
Width(createBoxWidth).
|
||||
BorderForeground(styles.PrimaryBorder).
|
||||
Render(createContent)
|
||||
return lipgloss.Place(m.width, m.height, lipgloss.Center, lipgloss.Center, createBox)
|
||||
}
|
||||
|
||||
if m.confirmingDelete {
|
||||
confirmContent := lipgloss.JoinVertical(lipgloss.Left,
|
||||
styles.LabelStyle.Render("Delete issue "+m.deleteConfirmID+"?"),
|
||||
lipgloss.NewStyle().Foreground(styles.FaintText).Render("Press y to delete, n or Esc to cancel"),
|
||||
)
|
||||
confirmBoxWidth := min(50, m.width-4)
|
||||
confirmBox := styles.ContainerStyle.
|
||||
Width(confirmBoxWidth).
|
||||
BorderForeground(styles.PrimaryBorder).
|
||||
Render(confirmContent)
|
||||
return lipgloss.Place(m.width, m.height, lipgloss.Center, lipgloss.Center, confirmBox)
|
||||
}
|
||||
|
||||
if m.choosingStatus {
|
||||
statusContent := lipgloss.JoinVertical(lipgloss.Left,
|
||||
styles.LabelStyle.Render("Change status for "+m.statusIssueID+":"),
|
||||
lipgloss.NewStyle().Foreground(styles.FaintText).Render("o = open i = in_progress c = closed"),
|
||||
lipgloss.NewStyle().Foreground(styles.FaintText).Render("Esc = cancel"),
|
||||
)
|
||||
statusBoxWidth := min(50, m.width-4)
|
||||
statusBox := styles.ContainerStyle.
|
||||
Width(statusBoxWidth).
|
||||
BorderForeground(styles.PrimaryBorder).
|
||||
Render(statusContent)
|
||||
return lipgloss.Place(m.width, m.height, lipgloss.Center, lipgloss.Center, statusBox)
|
||||
}
|
||||
|
||||
if m.choosingPriority {
|
||||
priorityContent := lipgloss.JoinVertical(lipgloss.Left,
|
||||
styles.LabelStyle.Render("Change priority for "+m.priorityIssueID+":"),
|
||||
lipgloss.NewStyle().Foreground(styles.FaintText).Render("0 = irrelevant 1 = low 2 = normal 3 = high 4 = critical"),
|
||||
lipgloss.NewStyle().Foreground(styles.FaintText).Render("Esc = cancel"),
|
||||
)
|
||||
priorityBoxWidth := min(60, m.width-4)
|
||||
priorityBox := styles.ContainerStyle.
|
||||
Width(priorityBoxWidth).
|
||||
BorderForeground(styles.PrimaryBorder).
|
||||
Render(priorityContent)
|
||||
return lipgloss.Place(m.width, m.height, lipgloss.Center, lipgloss.Center, priorityBox)
|
||||
}
|
||||
|
||||
if m.choosingType {
|
||||
typeContent := lipgloss.JoinVertical(lipgloss.Left,
|
||||
styles.LabelStyle.Render("Change type for "+m.typeIssueID+":"),
|
||||
lipgloss.NewStyle().Foreground(styles.FaintText).Render("b = bug f = feature t = task e = epic c = chore"),
|
||||
lipgloss.NewStyle().Foreground(styles.FaintText).Render("Esc = cancel"),
|
||||
)
|
||||
typeBoxWidth := min(65, m.width-4)
|
||||
typeBox := styles.ContainerStyle.
|
||||
Width(typeBoxWidth).
|
||||
BorderForeground(styles.PrimaryBorder).
|
||||
Render(typeContent)
|
||||
return lipgloss.Place(m.width, m.height, lipgloss.Center, lipgloss.Center, typeBox)
|
||||
}
|
||||
|
||||
return mainView
|
||||
|
||||
}
|
||||
|
||||
func (m *Model) footer() string {
|
||||
feedbackStatus := m.currentFeedback.Message
|
||||
|
||||
if feedbackStatus == "" {
|
||||
return m.helpBar.View()
|
||||
}
|
||||
|
||||
m.helpBar.SetWidth(m.width - lipgloss.Width(feedbackStatus))
|
||||
return lipgloss.JoinHorizontal(lipgloss.Left, m.helpBar.View(), feedbackStatus)
|
||||
}
|
||||
50
pkg/tui/views/root.go
Normal file
50
pkg/tui/views/root.go
Normal file
@@ -0,0 +1,50 @@
|
||||
package views
|
||||
|
||||
import (
|
||||
"github.com/LazyBachelor/LazyPM/internal/app"
|
||||
"github.com/LazyBachelor/LazyPM/internal/models"
|
||||
"github.com/LazyBachelor/LazyPM/pkg/tui/msgs"
|
||||
"github.com/LazyBachelor/LazyPM/pkg/tui/views/dashboard"
|
||||
"github.com/LazyBachelor/LazyPM/pkg/tui/views/dashboard2"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
)
|
||||
|
||||
type RootModel struct {
|
||||
currentView tea.Model
|
||||
app *app.App
|
||||
feedbackChan chan models.ValidationFeedback
|
||||
quitChan chan bool
|
||||
}
|
||||
|
||||
func NewRootView(app *app.App, feedbackChan chan models.ValidationFeedback, quitChan chan bool) *RootModel {
|
||||
initialView := dashboard.NewDashboard(app, feedbackChan, quitChan)
|
||||
return &RootModel{
|
||||
currentView: initialView,
|
||||
app: app,
|
||||
feedbackChan: feedbackChan,
|
||||
quitChan: quitChan,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *RootModel) Init() tea.Cmd {
|
||||
return r.currentView.Init()
|
||||
}
|
||||
|
||||
func (r *RootModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
switch msg.(type) {
|
||||
case msgs.SwitchToDashboard2Msg:
|
||||
r.currentView = dashboard2.NewDashboard(r.app, r.feedbackChan, r.quitChan)
|
||||
return r, r.currentView.Init()
|
||||
case msgs.SwitchToDashboardMsg:
|
||||
r.currentView = dashboard.NewDashboard(r.app, r.feedbackChan, r.quitChan)
|
||||
return r, r.currentView.Init()
|
||||
}
|
||||
|
||||
var cmd tea.Cmd
|
||||
r.currentView, cmd = r.currentView.Update(msg)
|
||||
return r, cmd
|
||||
}
|
||||
|
||||
func (r *RootModel) View() string {
|
||||
return r.currentView.View()
|
||||
}
|
||||
Reference in New Issue
Block a user