tagliatelle

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs

commit 157d0caf1452807dfa0c0c71aeb4e3846013d391
parent 0c50f105fcf0a895f43115cf024c0094c94ea446
Author: breadcat <breadcat@users.noreply.github.com>
Date:   Fri,  1 May 2026 09:23:41 +0100

Allow properties in /and/ filtering

Diffstat:
Minclude-filters.go | 121+++++++++++++++++++++++++++++++++++++++++++++++++++----------------------------
Minclude-previews.go | 8++++++++
Minclude-properties.go | 48++++++++++++++++++++++++++++++++++++++++++++++++
Minclude-types.go | 1+
4 files changed, 135 insertions(+), 43 deletions(-)

diff --git a/include-filters.go b/include-filters.go @@ -25,7 +25,15 @@ func buildTagFilterWhere(filters []filter) (string, []interface{}) { var args []interface{} for _, f := range filters { - if f.Value == "unassigned" { + if f.IsProperty { + where += ` + AND EXISTS ( + SELECT 1 + FROM file_properties fp + WHERE fp.file_id = f.id AND fp.key = ? AND fp.value = ? + )` + args = append(args, f.Category, f.Value) + } else if f.Value == "unassigned" { where += ` AND NOT EXISTS ( SELECT 1 @@ -61,12 +69,9 @@ func buildTagFilterWhere(filters []filter) (string, []interface{}) { return where, args } -func tagFilterHandler(w http.ResponseWriter, r *http.Request) { - page := pageFromRequest(r) - perPage := perPageFromConfig(50) - - fullPath := strings.TrimPrefix(r.URL.Path, "/tag/") - tagPairs := strings.Split(fullPath, "/and/tag/") +// parseFilterSegments splits filter path into individual filter structs +func parseFilterSegments(fullPath, firstKind string) ([]filter, []Breadcrumb, error) { + rawSegments := strings.Split(fullPath, "/and/") breadcrumbs := []Breadcrumb{ {Name: "home", URL: "/"}, @@ -74,58 +79,91 @@ func tagFilterHandler(w http.ResponseWriter, r *http.Request) { } var filters []filter - currentPath := "/tag" + currentPath := "/" + firstKind - for i, pair := range tagPairs { - parts := strings.Split(pair, "/") - if len(parts) != 2 { - renderError(w, "Invalid tag filter path", http.StatusBadRequest) - return + for i, seg := range rawSegments { + var kind, category, value string + + if i == 0 { + // First segment has no explicit kind prefix — the caller supplies it. + parts := strings.SplitN(seg, "/", 2) + if len(parts) != 2 || parts[0] == "" || parts[1] == "" { + return nil, nil, fmt.Errorf("invalid %s segment: %q", firstKind, seg) + } + kind, category, value = firstKind, parts[0], parts[1] + } else { + // Subsequent segments are "kind/category/value". + parts := strings.SplitN(seg, "/", 3) + if len(parts) != 3 || parts[0] == "" || parts[1] == "" || parts[2] == "" { + return nil, nil, fmt.Errorf("invalid filter segment: %q", seg) + } + kind, category, value = parts[0], parts[1], parts[2] + if kind != "tag" && kind != "property" { + return nil, nil, fmt.Errorf("unknown filter kind %q in segment: %q", kind, seg) + } } f := filter{ - Category: parts[0], - Value: parts[1], - IsPreviews: parts[1] == "previews", + Category: category, + Value: value, + IsProperty: kind == "property", + IsPreviews: kind == "tag" && value == "previews", } - // Expand with aliases (unless it's a special tag) - if parts[1] != "unassigned" && parts[1] != "previews" { - f.Values = expandTagWithAliases(parts[0], parts[1]) + if kind == "tag" && value != "unassigned" && value != "previews" { + f.Values = expandTagWithAliases(category, value) } filters = append(filters, f) - // Build breadcrumb path incrementally + // Build the cumulative breadcrumb URL for this filter step. if i == 0 { - currentPath += "/" + parts[0] + "/" + parts[1] + currentPath += "/" + category + "/" + value } else { - currentPath += "/and/tag/" + parts[0] + "/" + parts[1] + currentPath += "/and/" + kind + "/" + category + "/" + value } - // Add category breadcrumb (only if it's the first occurrence) + // Add a category/key breadcrumb (deduplicated). categoryExists := false for _, bc := range breadcrumbs { - if bc.Name == parts[0] { + if bc.Name == category { categoryExists = true break } } if !categoryExists { + anchorURL := "/tags#tag-" + category + if kind == "property" { + anchorURL = "/properties#prop-" + category + } breadcrumbs = append(breadcrumbs, Breadcrumb{ - Name: parts[0], - URL: "/tags#tag-" + parts[0], + Name: category, + URL: anchorURL, }) } - // Add value breadcrumb breadcrumbs = append(breadcrumbs, Breadcrumb{ - Name: parts[1], + Name: value, URL: currentPath, }) } - // Check if we're in preview mode for any filter + return filters, breadcrumbs, nil +} + +func tagFilterHandler(w http.ResponseWriter, r *http.Request) { + page := pageFromRequest(r) + perPage := perPageFromConfig(50) + + fullPath := strings.TrimPrefix(r.URL.Path, "/tag/") + + filters, breadcrumbs, err := parseFilterSegments(fullPath, "tag") + if err != nil { + renderError(w, "Invalid filter path", http.StatusBadRequest) + return + } + + // Check if we're in preview mode for any filter. hasPreviewFilter := false for _, f := range filters { if f.IsPreviews { @@ -135,7 +173,6 @@ func tagFilterHandler(w http.ResponseWriter, r *http.Request) { } if hasPreviewFilter { - // Handle preview mode files, err := getPreviewFiles(filters) if err != nil { log.Printf("Error: tagFilterHandler: failed to fetch preview files: %v", err) @@ -143,12 +180,7 @@ func tagFilterHandler(w http.ResponseWriter, r *http.Request) { return } - var titleParts []string - for _, f := range filters { - titleParts = append(titleParts, fmt.Sprintf("%s: %s", f.Category, f.Value)) - } - title := "Tagged: " + strings.Join(titleParts, " + ") - + title := "Tagged: " + buildFilterTitle(filters, " + ") pageData := buildPageDataWithPagination(title, ListData{ Tagged: files, Untagged: nil, @@ -165,7 +197,7 @@ func tagFilterHandler(w http.ResponseWriter, r *http.Request) { var total int countArgs := append([]interface{}(nil), whereArgs...) // copy; count query does not need pagination args - err := db.QueryRow(`SELECT COUNT(DISTINCT f.id) FROM files f`+where, countArgs...).Scan(&total) + err = db.QueryRow(`SELECT COUNT(DISTINCT f.id) FROM files f`+where, countArgs...).Scan(&total) if err != nil { log.Printf("Error: tagFilterHandler: failed to count files: %v", err) renderError(w, "Failed to count files", http.StatusInternalServerError) @@ -185,12 +217,7 @@ func tagFilterHandler(w http.ResponseWriter, r *http.Request) { return } - var titleParts []string - for _, f := range filters { - titleParts = append(titleParts, fmt.Sprintf("%s: %s", f.Category, f.Value)) - } - title := "Tagged: " + strings.Join(titleParts, ", ") - + title := "Tagged: " + buildFilterTitle(filters, ", ") pageData := buildPageDataWithPagination(title, ListData{ Tagged: files, Untagged: nil, @@ -201,6 +228,14 @@ func tagFilterHandler(w http.ResponseWriter, r *http.Request) { renderTemplate(w, "list.html", pageData) } +func buildFilterTitle(filters []filter, sep string) string { + var parts []string + for _, f := range filters { + parts = append(parts, fmt.Sprintf("%s: %s", f.Category, f.Value)) + } + return strings.Join(parts, sep) +} + func expandTagWithAliases(category, value string) []string { values := []string{value} diff --git a/include-previews.go b/include-previews.go @@ -71,6 +71,14 @@ func getPreviewFiles(filters []filter) ([]File, error) { WHERE ft.file_id = f.id AND c.name = ? AND t.value = ? )` args = append(args, filter.Category, tagValue) + } else if filter.IsProperty { + query += ` + AND EXISTS ( + SELECT 1 + FROM file_properties fp + WHERE fp.file_id = f.id AND fp.key = ? AND fp.value = ? + )` + args = append(args, filter.Category, filter.Value) } else if filter.Value == "unassigned" { query += ` AND NOT EXISTS ( diff --git a/include-properties.go b/include-properties.go @@ -179,6 +179,54 @@ func getPropertyNav() (map[string][]PropertyDisplay, error) { func propertyFilterHandler(w http.ResponseWriter, r *http.Request) { trimmed := strings.TrimPrefix(r.URL.Path, "/property/") + + // If path contains /and/ delegate to the shared multi-filter + if strings.Contains(trimmed, "/and/") { + page := pageFromRequest(r) + perPage := perPageFromConfig(50) + + filters, breadcrumbs, err := parseFilterSegments(trimmed, "property") + if err != nil { + renderError(w, "Invalid filter path", http.StatusBadRequest) + return + } + + where, whereArgs := buildTagFilterWhere(filters) + + var total int + countArgs := append([]interface{}(nil), whereArgs...) + err = db.QueryRow(`SELECT COUNT(DISTINCT f.id) FROM files f`+where, countArgs...).Scan(&total) + if err != nil { + log.Printf("Error: propertyFilterHandler: failed to count files: %v", err) + renderError(w, "Failed to count files", http.StatusInternalServerError) + return + } + + offset := (page - 1) * perPage + dataArgs := append(append([]interface{}(nil), whereArgs...), perPage, offset) + files, err := queryFilesWithTags( + `SELECT f.id, f.filename, f.path, COALESCE(f.description, '') as description FROM files f`+ + where+` ORDER BY f.id DESC LIMIT ? OFFSET ?`, + dataArgs..., + ) + if err != nil { + log.Printf("Error: propertyFilterHandler: failed to fetch files: %v", err) + renderError(w, "Failed to fetch files", http.StatusInternalServerError) + return + } + + title := buildFilterTitle(filters, ", ") + pageData := buildPageDataWithPagination(title, ListData{ + Tagged: files, + Untagged: nil, + Breadcrumbs: []Breadcrumb{}, + }, page, total, perPage, r) + pageData.Breadcrumbs = breadcrumbs + + renderTemplate(w, "list.html", pageData) + return + } + parts := strings.SplitN(trimmed, "/", 2) if len(parts) != 2 || parts[0] == "" || parts[1] == "" { renderError(w, "Invalid property filter path", http.StatusBadRequest) diff --git a/include-types.go b/include-types.go @@ -86,6 +86,7 @@ type filter struct { Value string Values []string // Expanded values including aliases IsPreviews bool // New field to indicate preview mode + IsProperty bool } type BulkTagFormData struct {