Restructure the UI into top-level tabs and a scrollable project tree, and persist optional project groups so demos and settings can organize many projects. Co-authored-by: Cursor <cursoragent@cursor.com>
118 lines
3.1 KiB
Go
118 lines
3.1 KiB
Go
package sqlite
|
|
|
|
import (
|
|
"database/sql"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
// ProjectGroup is a folder for organizing projects in the UI tree.
|
|
type ProjectGroup struct {
|
|
ID string `json:"id"`
|
|
Name string `json:"name"`
|
|
SortOrder int `json:"sortOrder"`
|
|
}
|
|
|
|
// ListProjectGroups returns all groups sorted by sort_order then name.
|
|
func (db *DB) ListProjectGroups() ([]ProjectGroup, error) {
|
|
rows, err := db.SQL.Query(`
|
|
SELECT id, name, sort_order FROM project_groups
|
|
ORDER BY sort_order ASC, name COLLATE NOCASE`)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("list project groups: %w", err)
|
|
}
|
|
defer rows.Close()
|
|
var out []ProjectGroup
|
|
for rows.Next() {
|
|
var g ProjectGroup
|
|
if err := rows.Scan(&g.ID, &g.Name, &g.SortOrder); err != nil {
|
|
return nil, err
|
|
}
|
|
out = append(out, g)
|
|
}
|
|
if out == nil {
|
|
out = []ProjectGroup{}
|
|
}
|
|
return out, rows.Err()
|
|
}
|
|
|
|
// CreateProjectGroup inserts a new group.
|
|
func (db *DB) CreateProjectGroup(name string) (ProjectGroup, error) {
|
|
name = strings.TrimSpace(name)
|
|
if name == "" {
|
|
return ProjectGroup{}, fmt.Errorf("name is required")
|
|
}
|
|
id := uuid.NewString()
|
|
var maxOrd sql.NullInt64
|
|
_ = db.SQL.QueryRow(`SELECT MAX(sort_order) FROM project_groups`).Scan(&maxOrd)
|
|
ord := 0
|
|
if maxOrd.Valid {
|
|
ord = int(maxOrd.Int64) + 1
|
|
}
|
|
now := formatTime(time.Now().UTC())
|
|
_, err := db.SQL.Exec(`INSERT INTO project_groups(id, name, sort_order, created_at) VALUES(?, ?, ?, ?)`,
|
|
id, name, ord, now)
|
|
if err != nil {
|
|
return ProjectGroup{}, fmt.Errorf("create project group: %w", err)
|
|
}
|
|
return ProjectGroup{ID: id, Name: name, SortOrder: ord}, nil
|
|
}
|
|
|
|
// RenameProjectGroup updates the group name.
|
|
func (db *DB) RenameProjectGroup(id, name string) error {
|
|
name = strings.TrimSpace(name)
|
|
if id == "" || name == "" {
|
|
return fmt.Errorf("id and name are required")
|
|
}
|
|
res, err := db.SQL.Exec(`UPDATE project_groups SET name = ? WHERE id = ?`, name, id)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
n, _ := res.RowsAffected()
|
|
if n == 0 {
|
|
return fmt.Errorf("group %s: not found", id)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// DeleteProjectGroup removes a group; projects in it become ungrouped.
|
|
func (db *DB) DeleteProjectGroup(id string) error {
|
|
if id == "" {
|
|
return fmt.Errorf("id is required")
|
|
}
|
|
tx, err := db.SQL.Begin()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer func() { _ = tx.Rollback() }()
|
|
if _, err := tx.Exec(`UPDATE projects SET group_id = NULL WHERE group_id = ?`, id); err != nil {
|
|
return fmt.Errorf("ungroup projects: %w", err)
|
|
}
|
|
res, err := tx.Exec(`DELETE FROM project_groups WHERE id = ?`, id)
|
|
if err != nil {
|
|
return fmt.Errorf("delete group %s: %w", id, err)
|
|
}
|
|
n, _ := res.RowsAffected()
|
|
if n == 0 {
|
|
return fmt.Errorf("group %s: not found", id)
|
|
}
|
|
return tx.Commit()
|
|
}
|
|
|
|
// GetProjectGroup returns one group.
|
|
func (db *DB) GetProjectGroup(id string) (ProjectGroup, error) {
|
|
var g ProjectGroup
|
|
err := db.SQL.QueryRow(`SELECT id, name, sort_order FROM project_groups WHERE id = ?`, id).
|
|
Scan(&g.ID, &g.Name, &g.SortOrder)
|
|
if err == sql.ErrNoRows {
|
|
return ProjectGroup{}, fmt.Errorf("group %s: not found", id)
|
|
}
|
|
if err != nil {
|
|
return ProjectGroup{}, err
|
|
}
|
|
return g, nil
|
|
}
|