feat(ui): 🎨 Implement schema and table management screens
* Add schema management screen with list and editor * Implement table management screen with list and editor * Create data operations for schema and table management * Define UI rules and guidelines for consistency * Ensure circular tab navigation and keyboard shortcuts * Add forms for creating and editing schemas and tables * Implement confirmation dialogs for destructive actions
This commit is contained in:
526
pkg/ui/load_save_screens.go
Normal file
526
pkg/ui/load_save_screens.go
Normal file
@@ -0,0 +1,526 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/gdamore/tcell/v2"
|
||||
"github.com/rivo/tview"
|
||||
|
||||
"git.warky.dev/wdevs/relspecgo/pkg/models"
|
||||
"git.warky.dev/wdevs/relspecgo/pkg/readers"
|
||||
rbun "git.warky.dev/wdevs/relspecgo/pkg/readers/bun"
|
||||
rdbml "git.warky.dev/wdevs/relspecgo/pkg/readers/dbml"
|
||||
rdctx "git.warky.dev/wdevs/relspecgo/pkg/readers/dctx"
|
||||
rdrawdb "git.warky.dev/wdevs/relspecgo/pkg/readers/drawdb"
|
||||
rdrizzle "git.warky.dev/wdevs/relspecgo/pkg/readers/drizzle"
|
||||
rgorm "git.warky.dev/wdevs/relspecgo/pkg/readers/gorm"
|
||||
rgraphql "git.warky.dev/wdevs/relspecgo/pkg/readers/graphql"
|
||||
rjson "git.warky.dev/wdevs/relspecgo/pkg/readers/json"
|
||||
rpgsql "git.warky.dev/wdevs/relspecgo/pkg/readers/pgsql"
|
||||
rprisma "git.warky.dev/wdevs/relspecgo/pkg/readers/prisma"
|
||||
rtypeorm "git.warky.dev/wdevs/relspecgo/pkg/readers/typeorm"
|
||||
ryaml "git.warky.dev/wdevs/relspecgo/pkg/readers/yaml"
|
||||
"git.warky.dev/wdevs/relspecgo/pkg/writers"
|
||||
wbun "git.warky.dev/wdevs/relspecgo/pkg/writers/bun"
|
||||
wdbml "git.warky.dev/wdevs/relspecgo/pkg/writers/dbml"
|
||||
wdctx "git.warky.dev/wdevs/relspecgo/pkg/writers/dctx"
|
||||
wdrawdb "git.warky.dev/wdevs/relspecgo/pkg/writers/drawdb"
|
||||
wdrizzle "git.warky.dev/wdevs/relspecgo/pkg/writers/drizzle"
|
||||
wgorm "git.warky.dev/wdevs/relspecgo/pkg/writers/gorm"
|
||||
wgraphql "git.warky.dev/wdevs/relspecgo/pkg/writers/graphql"
|
||||
wjson "git.warky.dev/wdevs/relspecgo/pkg/writers/json"
|
||||
wpgsql "git.warky.dev/wdevs/relspecgo/pkg/writers/pgsql"
|
||||
wprisma "git.warky.dev/wdevs/relspecgo/pkg/writers/prisma"
|
||||
wtypeorm "git.warky.dev/wdevs/relspecgo/pkg/writers/typeorm"
|
||||
wyaml "git.warky.dev/wdevs/relspecgo/pkg/writers/yaml"
|
||||
)
|
||||
|
||||
// LoadConfig holds the configuration for loading a database
|
||||
type LoadConfig struct {
|
||||
SourceType string
|
||||
FilePath string
|
||||
ConnString string
|
||||
}
|
||||
|
||||
// SaveConfig holds the configuration for saving a database
|
||||
type SaveConfig struct {
|
||||
TargetType string
|
||||
FilePath string
|
||||
ConnString string
|
||||
}
|
||||
|
||||
// showLoadScreen displays the database load screen
|
||||
func (se *SchemaEditor) showLoadScreen() {
|
||||
flex := tview.NewFlex().SetDirection(tview.FlexRow)
|
||||
|
||||
// Title
|
||||
title := tview.NewTextView().
|
||||
SetText("[::b]Load Database Schema").
|
||||
SetTextAlign(tview.AlignCenter).
|
||||
SetDynamicColors(true)
|
||||
|
||||
// Form
|
||||
form := tview.NewForm()
|
||||
form.SetBorder(true).SetTitle(" Load Configuration ").SetTitleAlign(tview.AlignLeft)
|
||||
|
||||
// Format selection
|
||||
formatOptions := []string{
|
||||
"dbml", "dctx", "drawdb", "graphql", "json", "yaml",
|
||||
"gorm", "bun", "drizzle", "prisma", "typeorm", "pgsql",
|
||||
}
|
||||
selectedFormat := 0
|
||||
currentFormat := formatOptions[selectedFormat]
|
||||
|
||||
// File path input
|
||||
filePath := ""
|
||||
connString := ""
|
||||
|
||||
form.AddDropDown("Format", formatOptions, 0, func(option string, index int) {
|
||||
selectedFormat = index
|
||||
currentFormat = option
|
||||
})
|
||||
|
||||
form.AddInputField("File Path", "", 50, nil, func(value string) {
|
||||
filePath = value
|
||||
})
|
||||
|
||||
form.AddInputField("Connection String", "", 50, nil, func(value string) {
|
||||
connString = value
|
||||
})
|
||||
|
||||
form.AddTextView("Help", getLoadHelpText(), 0, 5, true, false)
|
||||
|
||||
// Buttons
|
||||
form.AddButton("Load [l]", func() {
|
||||
se.loadDatabase(currentFormat, filePath, connString)
|
||||
})
|
||||
|
||||
form.AddButton("Create New [n]", func() {
|
||||
se.createNewDatabase()
|
||||
})
|
||||
|
||||
form.AddButton("Exit [q]", func() {
|
||||
se.app.Stop()
|
||||
})
|
||||
|
||||
// Keyboard shortcuts
|
||||
form.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
|
||||
switch event.Key() {
|
||||
case tcell.KeyEscape:
|
||||
se.app.Stop()
|
||||
return nil
|
||||
}
|
||||
switch event.Rune() {
|
||||
case 'l':
|
||||
se.loadDatabase(currentFormat, filePath, connString)
|
||||
return nil
|
||||
case 'n':
|
||||
se.createNewDatabase()
|
||||
return nil
|
||||
case 'q':
|
||||
se.app.Stop()
|
||||
return nil
|
||||
}
|
||||
return event
|
||||
})
|
||||
|
||||
// Tab navigation
|
||||
form.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
|
||||
if event.Key() == tcell.KeyEscape {
|
||||
se.app.Stop()
|
||||
return nil
|
||||
}
|
||||
if event.Rune() == 'l' || event.Rune() == 'n' || event.Rune() == 'q' {
|
||||
return event
|
||||
}
|
||||
return event
|
||||
})
|
||||
|
||||
flex.AddItem(title, 1, 0, false).
|
||||
AddItem(form, 0, 1, true)
|
||||
|
||||
se.pages.AddAndSwitchToPage("load-database", flex, true)
|
||||
}
|
||||
|
||||
// showSaveScreen displays the save database screen
|
||||
func (se *SchemaEditor) showSaveScreen() {
|
||||
flex := tview.NewFlex().SetDirection(tview.FlexRow)
|
||||
|
||||
// Title
|
||||
title := tview.NewTextView().
|
||||
SetText("[::b]Save Database Schema").
|
||||
SetTextAlign(tview.AlignCenter).
|
||||
SetDynamicColors(true)
|
||||
|
||||
// Form
|
||||
form := tview.NewForm()
|
||||
form.SetBorder(true).SetTitle(" Save Configuration ").SetTitleAlign(tview.AlignLeft)
|
||||
|
||||
// Format selection
|
||||
formatOptions := []string{
|
||||
"dbml", "dctx", "drawdb", "graphql", "json", "yaml",
|
||||
"gorm", "bun", "drizzle", "prisma", "typeorm", "pgsql",
|
||||
}
|
||||
selectedFormat := 0
|
||||
currentFormat := formatOptions[selectedFormat]
|
||||
|
||||
// File path input
|
||||
filePath := ""
|
||||
if se.saveConfig != nil {
|
||||
// Pre-populate with existing save config
|
||||
for i, format := range formatOptions {
|
||||
if format == se.saveConfig.TargetType {
|
||||
selectedFormat = i
|
||||
currentFormat = format
|
||||
break
|
||||
}
|
||||
}
|
||||
filePath = se.saveConfig.FilePath
|
||||
}
|
||||
|
||||
form.AddDropDown("Format", formatOptions, selectedFormat, func(option string, index int) {
|
||||
selectedFormat = index
|
||||
currentFormat = option
|
||||
})
|
||||
|
||||
form.AddInputField("File Path", filePath, 50, nil, func(value string) {
|
||||
filePath = value
|
||||
})
|
||||
|
||||
form.AddTextView("Help", getSaveHelpText(), 0, 5, true, false)
|
||||
|
||||
// Buttons
|
||||
form.AddButton("Save [s]", func() {
|
||||
se.saveDatabase(currentFormat, filePath)
|
||||
})
|
||||
|
||||
form.AddButton("Update Existing Database [u]", func() {
|
||||
// Use saveConfig if available, otherwise use loadConfig
|
||||
if se.saveConfig != nil {
|
||||
se.showUpdateExistingDatabaseConfirm()
|
||||
} else if se.loadConfig != nil {
|
||||
se.showUpdateExistingDatabaseConfirm()
|
||||
} else {
|
||||
se.showErrorDialog("Error", "No database source found. Use Save instead.")
|
||||
}
|
||||
})
|
||||
|
||||
form.AddButton("Back [b]", func() {
|
||||
se.pages.RemovePage("save-database")
|
||||
se.pages.SwitchToPage("main")
|
||||
})
|
||||
|
||||
// Keyboard shortcuts
|
||||
form.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
|
||||
switch event.Key() {
|
||||
case tcell.KeyEscape:
|
||||
se.pages.RemovePage("save-database")
|
||||
se.pages.SwitchToPage("main")
|
||||
return nil
|
||||
}
|
||||
switch event.Rune() {
|
||||
case 's':
|
||||
se.saveDatabase(currentFormat, filePath)
|
||||
return nil
|
||||
case 'u':
|
||||
// Use saveConfig if available, otherwise use loadConfig
|
||||
if se.saveConfig != nil {
|
||||
se.showUpdateExistingDatabaseConfirm()
|
||||
} else if se.loadConfig != nil {
|
||||
se.showUpdateExistingDatabaseConfirm()
|
||||
} else {
|
||||
se.showErrorDialog("Error", "No database source found. Use Save instead.")
|
||||
}
|
||||
return nil
|
||||
case 'b':
|
||||
se.pages.RemovePage("save-database")
|
||||
se.pages.SwitchToPage("main")
|
||||
return nil
|
||||
}
|
||||
return event
|
||||
})
|
||||
|
||||
flex.AddItem(title, 1, 0, false).
|
||||
AddItem(form, 0, 1, true)
|
||||
|
||||
se.pages.AddAndSwitchToPage("save-database", flex, true)
|
||||
}
|
||||
|
||||
// loadDatabase loads a database from the specified configuration
|
||||
func (se *SchemaEditor) loadDatabase(format, filePath, connString string) {
|
||||
// Validate input
|
||||
if format == "pgsql" {
|
||||
if connString == "" {
|
||||
se.showErrorDialog("Error", "Connection string is required for PostgreSQL")
|
||||
return
|
||||
}
|
||||
} else {
|
||||
if filePath == "" {
|
||||
se.showErrorDialog("Error", "File path is required for "+format)
|
||||
return
|
||||
}
|
||||
// Expand home directory
|
||||
if len(filePath) > 0 && filePath[0] == '~' {
|
||||
home, err := os.UserHomeDir()
|
||||
if err == nil {
|
||||
filePath = filepath.Join(home, filePath[1:])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create reader
|
||||
var reader readers.Reader
|
||||
switch format {
|
||||
case "dbml":
|
||||
reader = rdbml.NewReader(&readers.ReaderOptions{FilePath: filePath})
|
||||
case "dctx":
|
||||
reader = rdctx.NewReader(&readers.ReaderOptions{FilePath: filePath})
|
||||
case "drawdb":
|
||||
reader = rdrawdb.NewReader(&readers.ReaderOptions{FilePath: filePath})
|
||||
case "graphql":
|
||||
reader = rgraphql.NewReader(&readers.ReaderOptions{FilePath: filePath})
|
||||
case "json":
|
||||
reader = rjson.NewReader(&readers.ReaderOptions{FilePath: filePath})
|
||||
case "yaml":
|
||||
reader = ryaml.NewReader(&readers.ReaderOptions{FilePath: filePath})
|
||||
case "gorm":
|
||||
reader = rgorm.NewReader(&readers.ReaderOptions{FilePath: filePath})
|
||||
case "bun":
|
||||
reader = rbun.NewReader(&readers.ReaderOptions{FilePath: filePath})
|
||||
case "drizzle":
|
||||
reader = rdrizzle.NewReader(&readers.ReaderOptions{FilePath: filePath})
|
||||
case "prisma":
|
||||
reader = rprisma.NewReader(&readers.ReaderOptions{FilePath: filePath})
|
||||
case "typeorm":
|
||||
reader = rtypeorm.NewReader(&readers.ReaderOptions{FilePath: filePath})
|
||||
case "pgsql":
|
||||
reader = rpgsql.NewReader(&readers.ReaderOptions{ConnectionString: connString})
|
||||
default:
|
||||
se.showErrorDialog("Error", "Unsupported format: "+format)
|
||||
return
|
||||
}
|
||||
|
||||
// Read database
|
||||
db, err := reader.ReadDatabase()
|
||||
if err != nil {
|
||||
se.showErrorDialog("Load Error", fmt.Sprintf("Failed to load database: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
// Store load config
|
||||
se.loadConfig = &LoadConfig{
|
||||
SourceType: format,
|
||||
FilePath: filePath,
|
||||
ConnString: connString,
|
||||
}
|
||||
|
||||
// Update database
|
||||
se.db = db
|
||||
|
||||
// Show success and switch to main menu
|
||||
se.showSuccessDialog("Load Complete", fmt.Sprintf("Successfully loaded database '%s'", db.Name), func() {
|
||||
se.pages.RemovePage("load-database")
|
||||
se.pages.RemovePage("main")
|
||||
se.pages.AddPage("main", se.createMainMenu(), true, true)
|
||||
})
|
||||
}
|
||||
|
||||
// saveDatabase saves the database to the specified configuration
|
||||
func (se *SchemaEditor) saveDatabase(format, filePath string) {
|
||||
// Validate input
|
||||
if format == "pgsql" {
|
||||
se.showErrorDialog("Error", "Direct PostgreSQL save is not supported from the UI. Use --to pgsql --to-path output.sql")
|
||||
return
|
||||
}
|
||||
|
||||
if filePath == "" {
|
||||
se.showErrorDialog("Error", "File path is required")
|
||||
return
|
||||
}
|
||||
|
||||
// Expand home directory
|
||||
if len(filePath) > 0 && filePath[0] == '~' {
|
||||
home, err := os.UserHomeDir()
|
||||
if err == nil {
|
||||
filePath = filepath.Join(home, filePath[1:])
|
||||
}
|
||||
}
|
||||
|
||||
// Create writer
|
||||
var writer writers.Writer
|
||||
switch format {
|
||||
case "dbml":
|
||||
writer = wdbml.NewWriter(&writers.WriterOptions{OutputPath: filePath})
|
||||
case "dctx":
|
||||
writer = wdctx.NewWriter(&writers.WriterOptions{OutputPath: filePath})
|
||||
case "drawdb":
|
||||
writer = wdrawdb.NewWriter(&writers.WriterOptions{OutputPath: filePath})
|
||||
case "graphql":
|
||||
writer = wgraphql.NewWriter(&writers.WriterOptions{OutputPath: filePath})
|
||||
case "json":
|
||||
writer = wjson.NewWriter(&writers.WriterOptions{OutputPath: filePath})
|
||||
case "yaml":
|
||||
writer = wyaml.NewWriter(&writers.WriterOptions{OutputPath: filePath})
|
||||
case "gorm":
|
||||
writer = wgorm.NewWriter(&writers.WriterOptions{OutputPath: filePath})
|
||||
case "bun":
|
||||
writer = wbun.NewWriter(&writers.WriterOptions{OutputPath: filePath})
|
||||
case "drizzle":
|
||||
writer = wdrizzle.NewWriter(&writers.WriterOptions{OutputPath: filePath})
|
||||
case "prisma":
|
||||
writer = wprisma.NewWriter(&writers.WriterOptions{OutputPath: filePath})
|
||||
case "typeorm":
|
||||
writer = wtypeorm.NewWriter(&writers.WriterOptions{OutputPath: filePath})
|
||||
case "pgsql":
|
||||
writer = wpgsql.NewWriter(&writers.WriterOptions{OutputPath: filePath})
|
||||
default:
|
||||
se.showErrorDialog("Error", "Unsupported format: "+format)
|
||||
return
|
||||
}
|
||||
|
||||
// Write database
|
||||
err := writer.WriteDatabase(se.db)
|
||||
if err != nil {
|
||||
se.showErrorDialog("Save Error", fmt.Sprintf("Failed to save database: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
// Store save config
|
||||
se.saveConfig = &SaveConfig{
|
||||
TargetType: format,
|
||||
FilePath: filePath,
|
||||
}
|
||||
|
||||
// Show success
|
||||
se.showSuccessDialog("Save Complete", fmt.Sprintf("Successfully saved database to %s", filePath), func() {
|
||||
se.pages.RemovePage("save-database")
|
||||
se.pages.SwitchToPage("main")
|
||||
})
|
||||
}
|
||||
|
||||
// createNewDatabase creates a new empty database
|
||||
func (se *SchemaEditor) createNewDatabase() {
|
||||
// Create a new empty database
|
||||
se.db = &models.Database{
|
||||
Name: "New Database",
|
||||
Schemas: []*models.Schema{},
|
||||
}
|
||||
|
||||
// Clear load config
|
||||
se.loadConfig = nil
|
||||
|
||||
// Show success and switch to main menu
|
||||
se.showSuccessDialog("New Database", "Created new empty database", func() {
|
||||
se.pages.RemovePage("load-database")
|
||||
se.pages.AddPage("main", se.createMainMenu(), true, true)
|
||||
})
|
||||
}
|
||||
|
||||
// showErrorDialog displays an error dialog
|
||||
func (se *SchemaEditor) showErrorDialog(_title, message string) {
|
||||
modal := tview.NewModal().
|
||||
SetText(message).
|
||||
AddButtons([]string{"OK"}).
|
||||
SetDoneFunc(func(buttonIndex int, buttonLabel string) {
|
||||
se.pages.RemovePage("error-dialog")
|
||||
})
|
||||
|
||||
modal.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
|
||||
if event.Key() == tcell.KeyEscape {
|
||||
se.pages.RemovePage("error-dialog")
|
||||
return nil
|
||||
}
|
||||
return event
|
||||
})
|
||||
|
||||
se.pages.AddPage("error-dialog", modal, true, true)
|
||||
}
|
||||
|
||||
// showSuccessDialog displays a success dialog
|
||||
func (se *SchemaEditor) showSuccessDialog(_title, message string, onClose func()) {
|
||||
modal := tview.NewModal().
|
||||
SetText(message).
|
||||
AddButtons([]string{"OK"}).
|
||||
SetDoneFunc(func(buttonIndex int, buttonLabel string) {
|
||||
se.pages.RemovePage("success-dialog")
|
||||
if onClose != nil {
|
||||
onClose()
|
||||
}
|
||||
})
|
||||
|
||||
modal.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
|
||||
if event.Key() == tcell.KeyEscape {
|
||||
se.pages.RemovePage("success-dialog")
|
||||
if onClose != nil {
|
||||
onClose()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return event
|
||||
})
|
||||
|
||||
se.pages.AddPage("success-dialog", modal, true, true)
|
||||
}
|
||||
|
||||
// getLoadHelpText returns the help text for the load screen
|
||||
func getLoadHelpText() string {
|
||||
return `File-based formats: dbml, dctx, drawdb, graphql, json, yaml, gorm, bun, drizzle, prisma, typeorm
|
||||
Database formats: pgsql (requires connection string)
|
||||
|
||||
Examples:
|
||||
- File path: ~/schemas/mydb.dbml or /path/to/schema.json
|
||||
- Connection: postgres://user:pass@localhost/dbname`
|
||||
}
|
||||
|
||||
// showUpdateExistingDatabaseConfirm displays a confirmation dialog before updating existing database
|
||||
func (se *SchemaEditor) showUpdateExistingDatabaseConfirm() {
|
||||
// Use saveConfig if available, otherwise use loadConfig
|
||||
var targetType, targetPath string
|
||||
if se.saveConfig != nil {
|
||||
targetType = se.saveConfig.TargetType
|
||||
targetPath = se.saveConfig.FilePath
|
||||
} else if se.loadConfig != nil {
|
||||
targetType = se.loadConfig.SourceType
|
||||
targetPath = se.loadConfig.FilePath
|
||||
} else {
|
||||
return
|
||||
}
|
||||
|
||||
confirmText := fmt.Sprintf("Update existing database?\n\nFormat: %s\nPath: %s\n\nThis will overwrite the source.",
|
||||
targetType, targetPath)
|
||||
|
||||
modal := tview.NewModal().
|
||||
SetText(confirmText).
|
||||
AddButtons([]string{"Cancel", "Update"}).
|
||||
SetDoneFunc(func(buttonIndex int, buttonLabel string) {
|
||||
if buttonLabel == "Update" {
|
||||
se.pages.RemovePage("update-confirm")
|
||||
se.pages.RemovePage("save-database")
|
||||
se.saveDatabase(targetType, targetPath)
|
||||
se.pages.SwitchToPage("main")
|
||||
} else {
|
||||
se.pages.RemovePage("update-confirm")
|
||||
}
|
||||
})
|
||||
|
||||
modal.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
|
||||
if event.Key() == tcell.KeyEscape {
|
||||
se.pages.RemovePage("update-confirm")
|
||||
return nil
|
||||
}
|
||||
return event
|
||||
})
|
||||
|
||||
se.pages.AddAndSwitchToPage("update-confirm", modal, true)
|
||||
}
|
||||
|
||||
// getSaveHelpText returns the help text for the save screen
|
||||
func getSaveHelpText() string {
|
||||
return `File-based formats: dbml, dctx, drawdb, graphql, json, yaml, gorm, bun, drizzle, prisma, typeorm, pgsql (SQL export)
|
||||
|
||||
Examples:
|
||||
- File: ~/schemas/mydb.dbml
|
||||
- Directory (for code formats): ./models/`
|
||||
}
|
||||
Reference in New Issue
Block a user