Fix project selection and add project settings UI

This commit is contained in:
Mikhail Chusavitin
2026-02-13 12:51:53 +03:00
parent 857ec7a0e5
commit 4e1a46bd71
7 changed files with 191 additions and 22 deletions

View File

@@ -147,8 +147,8 @@ func (h *WebHandler) render(c *gin.Context, name string, data gin.H) {
}
func (h *WebHandler) Index(c *gin.Context) {
// Redirect to configs page - configurator is accessed via /configurator?uuid=...
c.Redirect(302, "/configs")
// Redirect to projects page - configurator is accessed via /configurator?uuid=...
c.Redirect(302, "/projects")
}
func (h *WebHandler) Configurator(c *gin.Context) {

View File

@@ -11,8 +11,8 @@ import (
"strings"
"time"
"git.mchus.pro/mchus/quoteforge/internal/appstate"
"git.mchus.pro/mchus/quoteforge/internal/appmeta"
"git.mchus.pro/mchus/quoteforge/internal/appstate"
"github.com/glebarez/sqlite"
mysqlDriver "github.com/go-sql-driver/mysql"
uuidpkg "github.com/google/uuid"
@@ -434,18 +434,36 @@ func (l *LocalDB) ListConfigurationsWithFilters(status string, search string, of
query := l.db.Model(&LocalConfiguration{})
switch status {
case "active":
query = query.Where("is_active = ?", true)
query = query.Where("local_configurations.is_active = ?", true)
case "archived":
query = query.Where("is_active = ?", false)
query = query.Where("local_configurations.is_active = ?", false)
case "all", "":
// no-op
default:
query = query.Where("is_active = ?", true)
query = query.Where("local_configurations.is_active = ?", true)
}
search = strings.TrimSpace(search)
if search != "" {
query = query.Where("LOWER(name) LIKE ?", "%"+strings.ToLower(search)+"%")
needle := "%" + strings.ToLower(search) + "%"
hasProjectsTable := l.db.Migrator().HasTable(&LocalProject{})
hasServerModel := l.db.Migrator().HasColumn(&LocalConfiguration{}, "server_model")
conditions := []string{"LOWER(local_configurations.name) LIKE ?"}
args := []interface{}{needle}
if hasProjectsTable {
query = query.Joins("LEFT JOIN local_projects lp ON lp.uuid = local_configurations.project_uuid")
conditions = append(conditions, "LOWER(COALESCE(lp.name, '')) LIKE ?")
args = append(args, needle)
}
if hasServerModel {
conditions = append(conditions, "LOWER(COALESCE(local_configurations.server_model, '')) LIKE ?")
args = append(args, needle)
}
query = query.Where(strings.Join(conditions, " OR "), args...)
}
var total int64
@@ -454,7 +472,7 @@ func (l *LocalDB) ListConfigurationsWithFilters(status string, search string, of
}
var configs []LocalConfiguration
if err := query.Order("created_at DESC").Offset(offset).Limit(limit).Find(&configs).Error; err != nil {
if err := query.Order("local_configurations.created_at DESC").Offset(offset).Limit(limit).Find(&configs).Error; err != nil {
return nil, 0, err
}
return configs, total, nil

View File

@@ -16,8 +16,9 @@ import (
)
var (
ErrProjectNotFound = errors.New("project not found")
ErrProjectForbidden = errors.New("access to project forbidden")
ErrProjectNotFound = errors.New("project not found")
ErrProjectForbidden = errors.New("access to project forbidden")
ErrProjectNameExists = errors.New("project name already exists")
)
type ProjectService struct {
@@ -49,6 +50,9 @@ func (s *ProjectService) Create(ownerUsername string, req *CreateProjectRequest)
if name == "" {
return nil, fmt.Errorf("project name is required")
}
if err := s.ensureUniqueProjectName("", name); err != nil {
return nil, err
}
now := time.Now()
localProject := &localdb.LocalProject{
@@ -81,6 +85,9 @@ func (s *ProjectService) Update(projectUUID, ownerUsername string, req *UpdatePr
if name == "" {
return nil, fmt.Errorf("project name is required")
}
if err := s.ensureUniqueProjectName(projectUUID, name); err != nil {
return nil, err
}
localProject.Name = name
if req.TrackerURL != nil {
@@ -99,6 +106,32 @@ func (s *ProjectService) Update(projectUUID, ownerUsername string, req *UpdatePr
return localdb.LocalToProject(localProject), nil
}
func (s *ProjectService) ensureUniqueProjectName(excludeUUID, name string) error {
normalized := normalizeProjectName(name)
if normalized == "" {
return fmt.Errorf("project name is required")
}
projects, err := s.localDB.GetAllProjects(true)
if err != nil {
return err
}
for i := range projects {
project := projects[i]
if excludeUUID != "" && project.UUID == excludeUUID {
continue
}
if normalizeProjectName(project.Name) == normalized {
return ErrProjectNameExists
}
}
return nil
}
func normalizeProjectName(name string) string {
return strings.ToLower(strings.TrimSpace(name))
}
func (s *ProjectService) Archive(projectUUID, ownerUsername string) error {
return s.setProjectActive(projectUUID, ownerUsername, false)
}