Files
wanderer/db/util/subcategory.go
slothful-vassal 3b8f00fd26 feat: Refine trail category model (#1059)
* feat: advanced trail categories

* rename remote_category

* fix merge issues

* subcategories for plugins mapping

* fix refresh

* remove vertical trail filter category scrolling

* cleanup

* add confirm modal for disabling a category

* Fix federation issues

* fix review findings

* redesign category settings page

* remove prio badge

* fix subcategory badge layout

* optimize subcategory settings layout

* further settings page layout optimization

* fix subcategory icon position

* fix

* update docs

---------

Co-authored-by: Christian Beutel <>
2026-06-29 03:16:26 +02:00

231 lines
5.6 KiB
Go

package util
import (
"fmt"
"github.com/pocketbase/pocketbase/core"
)
func ValidateSubcategoryRecord(app core.App, record *core.Record) error {
parentCategory := record.GetString("category")
if parentCategory == "" {
return fmt.Errorf("subcategory category is required")
}
name := record.GetString("name")
normalizedName := NormalizeCategoryName(name)
allSubcategories, err := app.FindAllRecords("subcategories")
if err != nil {
return err
}
for _, existing := range allSubcategories {
if existing.Id == record.Id || existing.GetString("category") != parentCategory {
continue
}
if NormalizeCategoryName(existing.GetString("name")) == normalizedName {
return fmt.Errorf("subcategory name %q collides with existing subcategory %q in the same category after normalization", name, existing.GetString("name"))
}
}
if _, err := ParseCategoryTranslations(record.Get("translations")); err != nil {
return err
}
return nil
}
func ValidateTrailSubcategoryRecord(app core.App, record *core.Record, subcategoryExplicit bool) error {
subcategoryID := record.GetString("subcategory")
if subcategoryID == "" {
return nil
}
categoryID := record.GetString("category")
if categoryID == "" {
if !subcategoryExplicit {
record.Set("subcategory", "")
return nil
}
return fmt.Errorf("trail subcategory requires a category")
}
subcategory, err := app.FindRecordById("subcategories", subcategoryID)
if err != nil {
if !subcategoryExplicit {
record.Set("subcategory", "")
return nil
}
return fmt.Errorf("trail subcategory %q does not exist: %w", subcategoryID, err)
}
parentCategory := subcategory.GetString("category")
if parentCategory != categoryID {
if !subcategoryExplicit {
record.Set("subcategory", "")
return nil
}
return fmt.Errorf("trail subcategory %q belongs to category %q, not %q", subcategoryID, parentCategory, categoryID)
}
return nil
}
func FindSubcategoryByNormalizedName(app core.App, categoryID string, name string) (*core.Record, error) {
normalizedName := NormalizeCategoryName(name)
if categoryID == "" || normalizedName == "" {
return nil, nil
}
subcategories, err := app.FindRecordsByFilter(
"subcategories",
"category = {:category}",
"",
0,
0,
map[string]any{"category": categoryID},
)
if err != nil {
return nil, err
}
for _, subcategory := range subcategories {
if NormalizeCategoryName(subcategory.GetString("name")) == normalizedName {
return subcategory, nil
}
}
return nil, nil
}
func ResolveCategoryAndSubcategoryByNormalizedNames(app core.App, categoryName string, subcategoryName string) (*core.Record, *core.Record, error) {
category, err := FindCategoryByNormalizedName(app, categoryName)
if err != nil || category == nil {
return category, nil, err
}
subcategory, err := FindSubcategoryByNormalizedName(app, category.Id, subcategoryName)
if err != nil {
return category, nil, err
}
return category, subcategory, nil
}
func BackfillRemoteTrailCategory(app core.App, category *core.Record) error {
if category == nil || category.Id == "" {
return nil
}
subcategoriesByName, err := normalizedSubcategoriesByName(app, category.Id)
if err != nil {
return err
}
trails, err := app.FindRecordsByFilter(
"trails",
"federated_category_name != '' && category = ''",
"",
0,
0,
nil,
)
if err != nil {
return err
}
normalizedCategoryName := NormalizeCategoryName(category.GetString("name"))
for _, trail := range trails {
if NormalizeCategoryName(trail.GetString("federated_category_name")) != normalizedCategoryName {
continue
}
trail.Set("category", category.Id)
if subcategory, ok := subcategoriesByName[NormalizeCategoryName(trail.GetString("federated_subcategory_name"))]; ok {
trail.Set("subcategory", subcategory.Id)
}
if err := app.Save(trail); err != nil {
return err
}
}
return nil
}
func BackfillRemoteTrailSubcategory(app core.App, subcategory *core.Record) error {
if subcategory == nil || subcategory.Id == "" {
return nil
}
category, err := app.FindRecordById("categories", subcategory.GetString("category"))
if err != nil {
return err
}
trails, err := app.FindRecordsByFilter(
"trails",
"federated_subcategory_name != '' && subcategory = '' && (category = {:category} || category = '')",
"",
0,
0,
map[string]any{"category": category.Id},
)
if err != nil {
return err
}
normalizedCategoryName := NormalizeCategoryName(category.GetString("name"))
normalizedSubcategoryName := NormalizeCategoryName(subcategory.GetString("name"))
for _, trail := range trails {
categoryID := trail.GetString("category")
if categoryID == "" {
if NormalizeCategoryName(trail.GetString("federated_category_name")) != normalizedCategoryName {
continue
}
trail.Set("category", category.Id)
}
if NormalizeCategoryName(trail.GetString("federated_subcategory_name")) != normalizedSubcategoryName {
continue
}
trail.Set("subcategory", subcategory.Id)
if err := app.Save(trail); err != nil {
return err
}
}
return nil
}
func normalizedSubcategoriesByName(app core.App, categoryID string) (map[string]*core.Record, error) {
subcategories, err := app.FindRecordsByFilter(
"subcategories",
"category = {:category}",
"",
0,
0,
map[string]any{"category": categoryID},
)
if err != nil {
return nil, err
}
byName := make(map[string]*core.Record, len(subcategories))
for _, subcategory := range subcategories {
normalizedName := NormalizeCategoryName(subcategory.GetString("name"))
if normalizedName == "" {
continue
}
byName[normalizedName] = subcategory
}
return byName, nil
}