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:
197
internal/tui/components/delete-confirm.go
Normal file
197
internal/tui/components/delete-confirm.go
Normal 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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user