mirror of
https://github.com/lukaszraczylo/lolcathost.git
synced 2026-06-05 23:29:18 +00:00
bugfix: Deleted hosts were lingering in the TUI list.
This commit is contained in:
@@ -219,6 +219,21 @@ func TestServer_HandleDelete(t *testing.T) {
|
||||
assert.Equal(t, "ok", resp.Status)
|
||||
})
|
||||
|
||||
t.Run("verify deleted entry not in list", func(t *testing.T) {
|
||||
// After delete, list should not contain the deleted entry
|
||||
resp := server.handleList()
|
||||
assert.Equal(t, "ok", resp.Status)
|
||||
|
||||
var data protocol.ListData
|
||||
err := resp.ParseData(&data)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Check that the deleted entry is not in the list
|
||||
for _, entry := range data.Entries {
|
||||
assert.NotEqual(t, "todelete", entry.Alias, "deleted entry should not appear in list")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("delete nonexistent", func(t *testing.T) {
|
||||
req, _ := protocol.NewRequest(protocol.RequestDelete, protocol.DeletePayload{
|
||||
Alias: "nonexistent",
|
||||
|
||||
+61
-2
@@ -9,6 +9,7 @@ import (
|
||||
|
||||
"github.com/charmbracelet/bubbles/textinput"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
|
||||
"github.com/lukaszraczylo/lolcathost/internal/client"
|
||||
"github.com/lukaszraczylo/lolcathost/internal/config"
|
||||
@@ -27,6 +28,7 @@ const (
|
||||
ViewBackups
|
||||
ViewHelp
|
||||
ViewSearch
|
||||
ViewConfirmDelete
|
||||
)
|
||||
|
||||
// Model is the main Bubble Tea model.
|
||||
@@ -52,6 +54,7 @@ type Model struct {
|
||||
messageTime time.Time
|
||||
searchTerm string
|
||||
allGroups []string // All groups including empty ones
|
||||
pendingDeleteAlias string // Alias of host pending delete confirmation
|
||||
|
||||
// Update notification
|
||||
updateAvailable bool
|
||||
@@ -352,7 +355,8 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
// Mark as disconnected to trigger reconnect
|
||||
m.connected = false
|
||||
m.client.Close()
|
||||
} else if msg.entries != nil {
|
||||
} else {
|
||||
// Always update the list, even if entries is nil/empty
|
||||
m.list.SetItems(msg.entries)
|
||||
}
|
||||
|
||||
@@ -385,7 +389,10 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
m.mode = ViewList
|
||||
|
||||
case deleteMsg:
|
||||
// Clear pending state regardless of success/failure
|
||||
m.list.SetPending(msg.alias, false)
|
||||
if msg.err != nil {
|
||||
m.list.SetError(msg.alias, true)
|
||||
m.setError(fmt.Sprintf("Delete failed: %v", msg.err))
|
||||
} else {
|
||||
cmds = append(cmds, m.refresh())
|
||||
@@ -521,6 +528,8 @@ func (m *Model) handleKey(msg tea.KeyMsg) tea.Cmd {
|
||||
return m.handleHelpKey(msg)
|
||||
case ViewSearch:
|
||||
return m.handleSearchKey(msg)
|
||||
case ViewConfirmDelete:
|
||||
return m.handleConfirmDeleteKey(msg)
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -554,7 +563,8 @@ func (m *Model) handleListKey(msg tea.KeyMsg) tea.Cmd {
|
||||
}
|
||||
case "d":
|
||||
if item := m.list.Selected(); item != nil {
|
||||
return m.deleteHost(item.Entry.Alias)
|
||||
m.pendingDeleteAlias = item.Entry.Alias
|
||||
m.mode = ViewConfirmDelete
|
||||
}
|
||||
case "p":
|
||||
m.mode = ViewPresets
|
||||
@@ -850,6 +860,23 @@ func (m *Model) handleSearchKey(msg tea.KeyMsg) tea.Cmd {
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (m *Model) handleConfirmDeleteKey(msg tea.KeyMsg) tea.Cmd {
|
||||
switch msg.String() {
|
||||
case "y", "Y":
|
||||
alias := m.pendingDeleteAlias
|
||||
m.pendingDeleteAlias = ""
|
||||
m.mode = ViewList
|
||||
// Set pending state for visual feedback
|
||||
m.list.SetPending(alias, true)
|
||||
return m.deleteHost(alias)
|
||||
case "n", "N", "esc":
|
||||
m.pendingDeleteAlias = ""
|
||||
m.mode = ViewList
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Model) toggleSelected() tea.Cmd {
|
||||
item := m.list.Selected()
|
||||
if item == nil {
|
||||
@@ -904,6 +931,8 @@ func (m *Model) View() string {
|
||||
sb.WriteString(m.helpView())
|
||||
case ViewSearch:
|
||||
sb.WriteString(m.searchView())
|
||||
case ViewConfirmDelete:
|
||||
sb.WriteString(m.confirmDeleteView())
|
||||
}
|
||||
|
||||
// Message
|
||||
@@ -1077,6 +1106,36 @@ func (m *Model) searchView() string {
|
||||
return dialogStyle.Render(sb.String())
|
||||
}
|
||||
|
||||
func (m *Model) confirmDeleteView() string {
|
||||
var sb strings.Builder
|
||||
|
||||
sb.WriteString(titleStyle.Render("Confirm Delete"))
|
||||
sb.WriteString("\n\n")
|
||||
|
||||
// Find the entry details for the pending delete
|
||||
var domain, ip string
|
||||
for _, item := range m.list.items {
|
||||
if item.Entry.Alias == m.pendingDeleteAlias {
|
||||
domain = item.Entry.Domain
|
||||
ip = item.Entry.IP
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
warningStyle := lipgloss.NewStyle().Foreground(colorWarning).Bold(true)
|
||||
sb.WriteString(warningStyle.Render("Are you sure you want to delete this host?"))
|
||||
sb.WriteString("\n\n")
|
||||
|
||||
sb.WriteString(fmt.Sprintf(" Alias: %s\n", helpKeyStyle.Render(m.pendingDeleteAlias)))
|
||||
sb.WriteString(fmt.Sprintf(" Domain: %s\n", helpDescStyle.Render(domain)))
|
||||
sb.WriteString(fmt.Sprintf(" IP: %s\n", helpDescStyle.Render(ip)))
|
||||
|
||||
sb.WriteString("\n")
|
||||
sb.WriteString(helpDescStyle.Render("y confirm • n/Esc cancel"))
|
||||
|
||||
return dialogStyle.Render(sb.String())
|
||||
}
|
||||
|
||||
// Run starts the TUI application.
|
||||
func Run(socketPath string) error {
|
||||
return RunWithVersion(socketPath, "dev", "", "")
|
||||
|
||||
@@ -407,3 +407,72 @@ func BenchmarkListView_View(b *testing.B) {
|
||||
_ = lv.View()
|
||||
}
|
||||
}
|
||||
|
||||
func TestListView_DeleteSimulation(t *testing.T) {
|
||||
lv := NewListView()
|
||||
|
||||
// Initial entries
|
||||
entries := []protocol.HostEntry{
|
||||
{Domain: "a.com", IP: "127.0.0.1", Alias: "a", Enabled: true, Group: "dev"},
|
||||
{Domain: "b.com", IP: "127.0.0.1", Alias: "b", Enabled: false, Group: "dev"},
|
||||
{Domain: "c.com", IP: "192.168.1.1", Alias: "c", Enabled: true, Group: "staging"},
|
||||
}
|
||||
lv.SetItems(entries)
|
||||
require.Equal(t, 3, lv.Len())
|
||||
|
||||
// Select the second item
|
||||
lv.MoveDown()
|
||||
selected := lv.Selected()
|
||||
require.NotNil(t, selected)
|
||||
require.Equal(t, "b", selected.Entry.Alias)
|
||||
|
||||
// Simulate delete: set new items without the deleted entry
|
||||
entriesAfterDelete := []protocol.HostEntry{
|
||||
{Domain: "a.com", IP: "127.0.0.1", Alias: "a", Enabled: true, Group: "dev"},
|
||||
{Domain: "c.com", IP: "192.168.1.1", Alias: "c", Enabled: true, Group: "staging"},
|
||||
}
|
||||
lv.SetItems(entriesAfterDelete)
|
||||
|
||||
// Verify list has only 2 items now
|
||||
assert.Equal(t, 2, lv.Len())
|
||||
|
||||
// Verify "b" is no longer in the list
|
||||
for i := 0; i < lv.Len(); i++ {
|
||||
assert.NotEqual(t, "b", lv.items[i].Entry.Alias, "deleted entry should not be in list")
|
||||
}
|
||||
|
||||
// Cursor should be adjusted if needed
|
||||
assert.LessOrEqual(t, lv.cursor, lv.Len()-1)
|
||||
}
|
||||
|
||||
func TestListView_SetItemsWithNil(t *testing.T) {
|
||||
lv := NewListView()
|
||||
|
||||
// Initial entries
|
||||
entries := []protocol.HostEntry{
|
||||
{Domain: "a.com", IP: "127.0.0.1", Alias: "a", Enabled: true, Group: "dev"},
|
||||
}
|
||||
lv.SetItems(entries)
|
||||
require.Equal(t, 1, lv.Len())
|
||||
|
||||
// Set nil entries (simulating empty list from server)
|
||||
lv.SetItems(nil)
|
||||
assert.Equal(t, 0, lv.Len())
|
||||
assert.Equal(t, 0, lv.cursor)
|
||||
}
|
||||
|
||||
func TestListView_SetItemsWithEmptySlice(t *testing.T) {
|
||||
lv := NewListView()
|
||||
|
||||
// Initial entries
|
||||
entries := []protocol.HostEntry{
|
||||
{Domain: "a.com", IP: "127.0.0.1", Alias: "a", Enabled: true, Group: "dev"},
|
||||
}
|
||||
lv.SetItems(entries)
|
||||
require.Equal(t, 1, lv.Len())
|
||||
|
||||
// Set empty slice
|
||||
lv.SetItems([]protocol.HostEntry{})
|
||||
assert.Equal(t, 0, lv.Len())
|
||||
assert.Equal(t, 0, lv.cursor)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user