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 <>
This commit is contained in:
230
db/util/subcategory.go
Normal file
230
db/util/subcategory.go
Normal file
@@ -0,0 +1,230 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user