Add project variants and UI updates

This commit is contained in:
Mikhail Chusavitin
2026-02-13 19:27:48 +03:00
parent 4e1a46bd71
commit 9b5d57902d
23 changed files with 1113 additions and 147 deletions

View File

@@ -200,6 +200,7 @@ func (s *Service) ImportProjectsToLocal() (*ProjectImportResult, error) {
}
existing.OwnerUsername = project.OwnerUsername
existing.Code = project.Code
existing.Name = project.Name
existing.TrackerURL = project.TrackerURL
existing.IsActive = project.IsActive
@@ -848,6 +849,12 @@ func (s *Service) pushProjectChange(change *localdb.PendingChange) error {
projectRepo := repository.NewProjectRepository(mariaDB)
project := payload.Snapshot
project.UUID = payload.ProjectUUID
if strings.TrimSpace(project.Code) == "" {
project.Code = strings.TrimSpace(derefString(project.Name))
if project.Code == "" {
project.Code = project.UUID
}
}
if err := projectRepo.UpsertByUUID(&project); err != nil {
return fmt.Errorf("upsert project on server: %w", err)
@@ -868,6 +875,17 @@ func (s *Service) pushProjectChange(change *localdb.PendingChange) error {
return nil
}
func derefString(value *string) string {
if value == nil {
return ""
}
return *value
}
func ptrString(value string) *string {
return &value
}
func decodeProjectChangePayload(change *localdb.PendingChange) (ProjectChangePayload, error) {
var payload ProjectChangePayload
if err := json.Unmarshal([]byte(change.Payload), &payload); err == nil && payload.ProjectUUID != "" {
@@ -1138,7 +1156,8 @@ func (s *Service) ensureConfigurationProject(mariaDB *gorm.DB, cfg *models.Confi
systemProject = &models.Project{
UUID: uuid.NewString(),
OwnerUsername: "",
Name: "Без проекта",
Code: "Без проекта",
Name: ptrString("Без проекта"),
IsActive: true,
IsSystem: true,
}
@@ -1302,6 +1321,21 @@ func (s *Service) loadCurrentConfigurationState(configurationUUID string) (model
}
}
if currentVersionNo == 0 {
if err := s.repairMissingConfigurationVersion(localCfg); err != nil {
return models.Configuration{}, "", 0, fmt.Errorf("repair missing configuration version: %w", err)
}
var latest localdb.LocalConfigurationVersion
err = s.localDB.DB().
Where("configuration_uuid = ?", configurationUUID).
Order("version_no DESC").
First(&latest).Error
if err == nil {
currentVersionNo = latest.VersionNo
currentVersionID = latest.ID
}
}
if currentVersionNo == 0 {
return models.Configuration{}, "", 0, fmt.Errorf("no local configuration version found for %s", configurationUUID)
}
@@ -1309,6 +1343,64 @@ func (s *Service) loadCurrentConfigurationState(configurationUUID string) (model
return cfg, currentVersionID, currentVersionNo, nil
}
func (s *Service) repairMissingConfigurationVersion(localCfg *localdb.LocalConfiguration) error {
if localCfg == nil {
return fmt.Errorf("local configuration is nil")
}
return s.localDB.DB().Transaction(func(tx *gorm.DB) error {
var cfg localdb.LocalConfiguration
if err := tx.Where("uuid = ?", localCfg.UUID).First(&cfg).Error; err != nil {
return fmt.Errorf("load local configuration: %w", err)
}
// If versions exist, just make sure current_version_id is set.
var latest localdb.LocalConfigurationVersion
if err := tx.Where("configuration_uuid = ?", cfg.UUID).
Order("version_no DESC").
First(&latest).Error; err == nil {
if cfg.CurrentVersionID == nil || *cfg.CurrentVersionID == "" {
if err := tx.Model(&localdb.LocalConfiguration{}).
Where("uuid = ?", cfg.UUID).
Update("current_version_id", latest.ID).Error; err != nil {
return fmt.Errorf("set current version id: %w", err)
}
}
return nil
} else if !errors.Is(err, gorm.ErrRecordNotFound) {
return fmt.Errorf("load latest version: %w", err)
}
snapshot, err := localdb.BuildConfigurationSnapshot(&cfg)
if err != nil {
return fmt.Errorf("build configuration snapshot: %w", err)
}
note := "Auto-repaired missing local version"
version := localdb.LocalConfigurationVersion{
ID: uuid.NewString(),
ConfigurationUUID: cfg.UUID,
VersionNo: 1,
Data: snapshot,
ChangeNote: &note,
AppVersion: appmeta.Version(),
CreatedAt: time.Now(),
}
if err := tx.Create(&version).Error; err != nil {
return fmt.Errorf("create initial version: %w", err)
}
if err := tx.Model(&localdb.LocalConfiguration{}).
Where("uuid = ?", cfg.UUID).
Update("current_version_id", version.ID).Error; err != nil {
return fmt.Errorf("set current version id: %w", err)
}
slog.Warn("repaired missing local configuration version", "uuid", cfg.UUID, "version_no", version.VersionNo)
return nil
})
}
// NOTE: prepared for future conflict resolution:
// when server starts storing version metadata, we can compare payload.CurrentVersionNo
// against remote version and branch into custom strategies. For now use last-write-wins.

View File

@@ -23,7 +23,7 @@ func TestPushPendingChangesProjectsBeforeConfigurations(t *testing.T) {
projectService := services.NewProjectService(local)
configService := services.NewLocalConfigurationService(local, localSync, &services.QuoteService{}, func() bool { return false })
project, err := projectService.Create("tester", &services.CreateProjectRequest{Name: "Project A"})
project, err := projectService.Create("tester", &services.CreateProjectRequest{Name: ptrString("Project A"), Code: "PRJ-A"})
if err != nil {
t.Fatalf("create project: %v", err)
}
@@ -74,11 +74,11 @@ func TestPushPendingChangesProjectCreateThenUpdateBeforeFirstPush(t *testing.T)
configService := services.NewLocalConfigurationService(local, localSync, &services.QuoteService{}, func() bool { return false })
pushService := syncsvc.NewServiceWithDB(serverDB, local)
project, err := projectService.Create("tester", &services.CreateProjectRequest{Name: "Project v1"})
project, err := projectService.Create("tester", &services.CreateProjectRequest{Name: ptrString("Project v1"), Code: "PRJ-V1"})
if err != nil {
t.Fatalf("create project: %v", err)
}
if _, err := projectService.Update(project.UUID, "tester", &services.UpdateProjectRequest{Name: "Project v2"}); err != nil {
if _, err := projectService.Update(project.UUID, "tester", &services.UpdateProjectRequest{Name: ptrString("Project v2")}); err != nil {
t.Fatalf("update project: %v", err)
}
@@ -100,8 +100,8 @@ func TestPushPendingChangesProjectCreateThenUpdateBeforeFirstPush(t *testing.T)
if err := serverDB.Where("uuid = ?", project.UUID).First(&serverProject).Error; err != nil {
t.Fatalf("project not pushed to server: %v", err)
}
if serverProject.Name != "Project v2" {
t.Fatalf("expected latest project name, got %q", serverProject.Name)
if serverProject.Name == nil || *serverProject.Name != "Project v2" {
t.Fatalf("expected latest project name, got %v", serverProject.Name)
}
var serverCfg models.Configuration
@@ -324,6 +324,8 @@ CREATE TABLE qt_projects (
id INTEGER PRIMARY KEY AUTOINCREMENT,
uuid TEXT NOT NULL UNIQUE,
owner_username TEXT NOT NULL,
code TEXT NOT NULL,
variant TEXT NOT NULL DEFAULT '',
name TEXT NOT NULL,
tracker_url TEXT NULL,
is_active INTEGER NOT NULL DEFAULT 1,
@@ -333,6 +335,9 @@ CREATE TABLE qt_projects (
);`).Error; err != nil {
t.Fatalf("create qt_projects: %v", err)
}
if err := db.Exec(`CREATE UNIQUE INDEX idx_qt_projects_code_variant ON qt_projects(code, variant);`).Error; err != nil {
t.Fatalf("create qt_projects index: %v", err)
}
if err := db.Exec(`
CREATE TABLE qt_configurations (
id INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -364,6 +369,10 @@ CREATE TABLE qt_configurations (
return db
}
func ptrString(value string) *string {
return &value
}
func getCurrentVersionInfo(t *testing.T, local *localdb.LocalDB, configurationUUID string, currentVersionID *string) (int, string) {
t.Helper()
if currentVersionID == nil || *currentVersionID == "" {