fix: change back navigation to 'b' key and improve deletion confirmation with name verification

- Changed 'q' key to 'b' for back navigation in client details
- Added 'esc' key binding for back navigation
- Updated help text to reflect new key bindings
- Created new DeleteConfirmModal component with name verification
- User must type exact client name to confirm deletion (safety feature)
- Improved modal styling with visual feedback (red/green indicators)
- Case-sensitive name matching to prevent accidental deletions

Fixes: wg-admin-az7
This commit is contained in:
Calmcacil
2026-01-12 22:17:27 +01:00
parent 0798b72858
commit d2dc361620
2 changed files with 202 additions and 15 deletions

View File

@@ -0,0 +1,197 @@
package components
import (
"fmt"
"github.com/charmbracelet/bubbles/textinput"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
)
// DeleteConfirmModel represents a deletion confirmation modal with name verification
type DeleteConfirmModel struct {
clientName string
input textinput.Model
Visible bool
Width int
Height int
showErrorMessage bool
}
// Styles
var (
deleteModalStyle = lipgloss.NewStyle().
Border(lipgloss.RoundedBorder()).
BorderForeground(lipgloss.Color("196")).
Padding(1, 3).
Background(lipgloss.Color("235"))
deleteTitleStyle = lipgloss.NewStyle().
Foreground(lipgloss.Color("226")).
Bold(true)
deleteMessageStyle = lipgloss.NewStyle().
Foreground(lipgloss.Color("255")).
Width(60)
deleteWarningStyle = lipgloss.NewStyle().
Foreground(lipgloss.Color("196")).
Bold(true).
MarginTop(1)
deleteInputStyle = lipgloss.NewStyle().
Foreground(lipgloss.Color("255")).
Width(60)
deleteHelpStyle = lipgloss.NewStyle().
Foreground(lipgloss.Color("241")).
MarginTop(1)
deleteErrorStyle = lipgloss.NewStyle().
Foreground(lipgloss.Color("196")).
Bold(true).
MarginTop(1)
deleteSuccessStyle = lipgloss.NewStyle().
Foreground(lipgloss.Color("46")).
Bold(true).
MarginTop(1)
)
// NewDeleteConfirm creates a new deletion confirmation modal
func NewDeleteConfirm(clientName string, width, height int) *DeleteConfirmModel {
ti := textinput.New()
ti.Placeholder = "Type client name to confirm"
ti.Focus()
ti.CharLimit = 100
ti.Width = 40
return &DeleteConfirmModel{
clientName: clientName,
input: ti,
Visible: true,
Width: width,
Height: height,
showErrorMessage: false,
}
}
// Init initializes the deletion confirmation modal
func (m *DeleteConfirmModel) Init() tea.Cmd {
return textinput.Blink
}
// Update handles messages for the deletion confirmation modal
func (m *DeleteConfirmModel) Update(msg tea.Msg) (*DeleteConfirmModel, tea.Cmd) {
var cmd tea.Cmd
switch msg := msg.(type) {
case tea.KeyMsg:
switch msg.String() {
case "esc":
m.Visible = false
return m, nil
case "enter":
if m.input.Value() == m.clientName {
m.Visible = false
return m, nil
}
m.showErrorMessage = true
return m, nil
}
}
m.input, cmd = m.input.Update(msg)
return m, cmd
}
// View renders the deletion confirmation modal
func (m *DeleteConfirmModel) View() string {
if !m.Visible {
return ""
}
// Check if input matches client name
matches := m.input.Value() == m.clientName
// Build warning section
warningText := deleteWarningStyle.Render(
fmt.Sprintf("⚠️ This will permanently delete client '%s'", m.clientName),
)
// Build message section
messageText := deleteMessageStyle.Render(
fmt.Sprintf("This action cannot be undone. Please type the client name to confirm deletion."),
)
// Build input section
inputSection := lipgloss.JoinVertical(
lipgloss.Left,
"",
deleteInputStyle.Render("Client name:"),
m.input.View(),
)
// Build status section
var statusText string
if matches {
statusText = deleteSuccessStyle.Render("✓ Client name matches. Press Enter to confirm deletion.")
} else if m.showErrorMessage {
statusText = deleteErrorStyle.Render("✗ Client name does not match. Please try again.")
} else if m.input.Value() != "" {
statusText = deleteHelpStyle.Render("Client name does not match yet...")
} else {
statusText = deleteHelpStyle.Render("Type the client name to enable confirmation.")
}
// Build help section
helpText := deleteHelpStyle.Render("Esc to cancel • Enter to confirm (when name matches)")
// Build modal content
content := lipgloss.JoinVertical(
lipgloss.Left,
deleteTitleStyle.Render("🗑️ Delete Client"),
"",
warningText,
"",
messageText,
inputSection,
statusText,
"",
helpText,
)
// Apply modal style
modal := deleteModalStyle.Render(content)
// Center modal on screen
modalWidth := lipgloss.Width(modal)
modalHeight := lipgloss.Height(modal)
x := (m.Width - modalWidth) / 2
if x < 0 {
x = 0
}
y := (m.Height - modalHeight) / 2
if y < 0 {
y = 0
}
return lipgloss.Place(m.Width, m.Height,
lipgloss.Left, lipgloss.Top,
modal,
lipgloss.WithWhitespaceChars(" "),
lipgloss.WithWhitespaceForeground(lipgloss.Color("235")),
)
}
// IsConfirmed returns true if user confirmed the deletion
func (m *DeleteConfirmModel) IsConfirmed() bool {
return !m.Visible && m.input.Value() == m.clientName
}
// IsCancelled returns true if user cancelled
func (m *DeleteConfirmModel) IsCancelled() bool {
return !m.Visible && m.input.Value() != m.clientName
}
// Reset resets the modal for reuse
func (m *DeleteConfirmModel) Reset() {
m.input.SetValue("")
m.input.Reset()
m.Visible = true
m.showErrorMessage = false
}

View File

@@ -17,7 +17,7 @@ type DetailScreen struct {
lastHandshake time.Time lastHandshake time.Time
transferRx string transferRx string
transferTx string transferTx string
confirmModal *components.ConfirmModel confirmModal *components.DeleteConfirmModel
showConfirm bool showConfirm bool
clipboardCopied bool clipboardCopied bool
clipboardTimer int clipboardTimer int
@@ -97,16 +97,6 @@ func (s *DetailScreen) Update(msg tea.Msg) (Screen, tea.Cmd) {
return s, nil return s, nil
} }
// Handle Enter key to confirm
switch msg := msg.(type) {
case tea.KeyMsg:
if msg.String() == "enter" && s.confirmModal.IsConfirmed() {
return s, tea.Batch(s.deleteClient(), func() tea.Msg {
return CloseDetailScreenMsg{}
})
}
}
return s, cmd return s, cmd
} }
@@ -121,13 +111,13 @@ func (s *DetailScreen) Update(msg tea.Msg) (Screen, tea.Cmd) {
s.transferTx = msg.transferTx s.transferTx = msg.transferTx
case tea.KeyMsg: case tea.KeyMsg:
switch msg.String() { switch msg.String() {
case "q", "esc": case "b", "esc":
// Return to list screen - signal parent to switch screens // Return to list screen - signal parent to switch screens
return s, nil return s, nil
case "d": case "d":
// Show delete confirmation // Show delete confirmation
s.confirmModal = components.NewConfirm( s.confirmModal = components.NewDeleteConfirm(
fmt.Sprintf("Are you sure you want to delete client '%s'?\n\nThis action cannot be undone.", s.client.Name), s.client.Name,
80, 80,
24, 24,
) )
@@ -196,7 +186,7 @@ func (s *DetailScreen) renderContent() string {
) )
// Add help text // Add help text
helpText := detailHelpStyle.Render("Actions: [d] Delete • [c] Copy Public Key • [q] Back") helpText := detailHelpStyle.Render("Actions: [d] Delete • [c] Copy Public Key • [b] Back")
content = lipgloss.JoinVertical(lipgloss.Left, content, helpText) content = lipgloss.JoinVertical(lipgloss.Left, content, helpText)
// Show clipboard confirmation // Show clipboard confirmation