mirror of
https://github.com/photoprism/photoprism.git
synced 2025-12-12 08:44:04 +01:00
Batch: Add helpful Items receivers to values_item.go
Signed-off-by: Michael Mayer <michael@photoprism.app>
This commit is contained in:
@@ -4,21 +4,6 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// Items represents batch edit value items.
|
||||
type Items struct {
|
||||
Items []Item `json:"items"`
|
||||
Mixed bool `json:"mixed"`
|
||||
Action Action `json:"action"`
|
||||
}
|
||||
|
||||
// Item represents batch edit value item.
|
||||
type Item struct {
|
||||
Value string `json:"value"`
|
||||
Title string `json:"title"`
|
||||
Mixed bool `json:"mixed"`
|
||||
Action Action `json:"action"`
|
||||
}
|
||||
|
||||
// String represents batch edit form value.
|
||||
type String struct {
|
||||
Value string `json:"value"`
|
||||
|
||||
128
internal/photoprism/batch/values_item.go
Normal file
128
internal/photoprism/batch/values_item.go
Normal file
@@ -0,0 +1,128 @@
|
||||
package batch
|
||||
|
||||
// Item represents batch edit value item.
|
||||
type Item struct {
|
||||
Value string `json:"value"`
|
||||
Title string `json:"title"`
|
||||
Mixed bool `json:"mixed"`
|
||||
Action Action `json:"action"`
|
||||
}
|
||||
|
||||
// Items represents batch edit value items.
|
||||
type Items struct {
|
||||
Items []Item `json:"items"`
|
||||
Mixed bool `json:"mixed"`
|
||||
Action Action `json:"action"`
|
||||
}
|
||||
|
||||
// ResolveValuesByTitle replaces empty values with resolver results so callers
|
||||
// can pre-create referenced entities (for example albums) once per unique
|
||||
// title instead of repeating the same lookup for every photo.
|
||||
func (it *Items) ResolveValuesByTitle(resolver func(title, action string) string) {
|
||||
if it == nil || resolver == nil || len(it.Items) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
type cacheKey struct {
|
||||
title string
|
||||
action Action
|
||||
}
|
||||
|
||||
cache := make(map[cacheKey]string)
|
||||
|
||||
for i := range it.Items {
|
||||
item := &it.Items[i]
|
||||
if item.Value != "" || item.Title == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
key := cacheKey{title: item.Title, action: item.Action}
|
||||
|
||||
if val, ok := cache[key]; ok {
|
||||
if val != "" {
|
||||
item.Value = val
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
resolved := resolver(item.Title, item.Action)
|
||||
cache[key] = resolved
|
||||
if resolved != "" {
|
||||
item.Value = resolved
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// GetValuesByActions returns the non-empty values for items whose action is
|
||||
// included in the provided filter. The original ordering is preserved so the
|
||||
// caller can correlate results with the source payload.
|
||||
func (it *Items) GetValuesByActions(actions []Action) []string {
|
||||
if it == nil || len(it.Items) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
actionFilter := actionSet(actions)
|
||||
if len(actionFilter) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
values := make([]string, 0, len(it.Items))
|
||||
|
||||
for _, item := range it.Items {
|
||||
if _, ok := actionFilter[item.Action]; !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
if item.Value == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
values = append(values, item.Value)
|
||||
}
|
||||
|
||||
if len(values) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
return values
|
||||
}
|
||||
|
||||
// GetItemsByActions returns all items whose action matches any entry in the
|
||||
// provided filter, preserving their original order.
|
||||
func (it *Items) GetItemsByActions(actions []Action) []Item {
|
||||
if it == nil || len(it.Items) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
actionFilter := actionSet(actions)
|
||||
if len(actionFilter) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
filtered := make([]Item, 0, len(it.Items))
|
||||
|
||||
for _, item := range it.Items {
|
||||
if _, ok := actionFilter[item.Action]; ok {
|
||||
filtered = append(filtered, item)
|
||||
}
|
||||
}
|
||||
|
||||
if len(filtered) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
return filtered
|
||||
}
|
||||
|
||||
func actionSet(actions []Action) map[Action]struct{} {
|
||||
if len(actions) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
m := make(map[Action]struct{}, len(actions))
|
||||
for _, act := range actions {
|
||||
m[act] = struct{}{}
|
||||
}
|
||||
|
||||
return m
|
||||
}
|
||||
140
internal/photoprism/batch/values_item_test.go
Normal file
140
internal/photoprism/batch/values_item_test.go
Normal file
@@ -0,0 +1,140 @@
|
||||
package batch
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestItemsGetValuesByActions(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
items := Items{
|
||||
Items: []Item{
|
||||
{Value: "uid-add", Action: ActionAdd},
|
||||
{Value: "uid-remove", Action: ActionRemove},
|
||||
{Value: "", Action: ActionAdd},
|
||||
{Value: "uid-update", Action: ActionUpdate},
|
||||
},
|
||||
}
|
||||
|
||||
want := []string{"uid-add", "uid-remove"}
|
||||
got := items.GetValuesByActions([]Action{ActionAdd, ActionRemove})
|
||||
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("GetValuesByActions() = %v, want %v", got, want)
|
||||
}
|
||||
|
||||
if vals := items.GetValuesByActions(nil); vals != nil {
|
||||
t.Fatalf("expected nil slice when no actions provided, got %v", vals)
|
||||
}
|
||||
|
||||
var empty *Items
|
||||
if vals := empty.GetValuesByActions([]Action{ActionAdd}); vals != nil {
|
||||
t.Fatalf("expected nil slice for nil receiver, got %v", vals)
|
||||
}
|
||||
}
|
||||
|
||||
func TestItemsGetItemsByActions(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
items := Items{
|
||||
Items: []Item{
|
||||
{Value: "uid-add", Title: "Add", Action: ActionAdd},
|
||||
{Value: "uid-remove", Title: "Remove", Action: ActionRemove},
|
||||
{Value: "uid-skip", Title: "Skip", Action: ActionNone},
|
||||
},
|
||||
}
|
||||
|
||||
want := []Item{
|
||||
{Value: "uid-add", Title: "Add", Action: ActionAdd},
|
||||
{Value: "uid-remove", Title: "Remove", Action: ActionRemove},
|
||||
}
|
||||
|
||||
got := items.GetItemsByActions([]Action{ActionAdd, ActionRemove})
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("GetItemsByActions() = %v, want %v", got, want)
|
||||
}
|
||||
|
||||
if filtered := items.GetItemsByActions([]Action{ActionUpdate}); filtered != nil {
|
||||
t.Fatalf("expected nil when no items match, got %v", filtered)
|
||||
}
|
||||
|
||||
var nilItems *Items
|
||||
if filtered := nilItems.GetItemsByActions([]Action{ActionAdd}); filtered != nil {
|
||||
t.Fatalf("expected nil for nil receiver, got %v", filtered)
|
||||
}
|
||||
}
|
||||
|
||||
func TestItemsResolveValuesByTitle(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
items := Items{
|
||||
Items: []Item{
|
||||
{Title: "Trips", Action: ActionRemove},
|
||||
{Title: "Trips", Action: ActionAdd},
|
||||
{Title: "Trips", Action: ActionAdd},
|
||||
{Title: "Archived", Value: "album-arch", Action: ActionAdd},
|
||||
{Title: "", Action: ActionAdd},
|
||||
{Title: "Drafts", Action: ActionAdd},
|
||||
{Title: "Trips", Action: ActionRemove},
|
||||
{Title: "Trips", Action: ActionUpdate},
|
||||
},
|
||||
}
|
||||
|
||||
var callLog []string
|
||||
resolver := func(title, action string) string {
|
||||
callLog = append(callLog, title+":"+action)
|
||||
if action != ActionAdd {
|
||||
return ""
|
||||
}
|
||||
|
||||
resolved := map[string]string{
|
||||
"Trips": "album-trips",
|
||||
"Drafts": "",
|
||||
}[title]
|
||||
return resolved
|
||||
}
|
||||
|
||||
items.ResolveValuesByTitle(resolver)
|
||||
|
||||
expectedCalls := []string{"Trips:remove", "Trips:add", "Drafts:add", "Trips:update"}
|
||||
if !reflect.DeepEqual(callLog, expectedCalls) {
|
||||
t.Fatalf("unexpected resolver calls %v, want %v", callLog, expectedCalls)
|
||||
}
|
||||
|
||||
if got := items.Items[0].Value; got != "" {
|
||||
t.Fatalf("expected remove action to remain empty, got %q", got)
|
||||
}
|
||||
|
||||
if got := items.Items[1].Value; got != "album-trips" {
|
||||
t.Fatalf("expected first add action to resolve value, got %q", got)
|
||||
}
|
||||
|
||||
if got := items.Items[2].Value; got != "album-trips" {
|
||||
t.Fatalf("expected duplicate add to reuse cached value, got %q", got)
|
||||
}
|
||||
|
||||
if got := items.Items[3].Value; got != "album-arch" {
|
||||
t.Fatalf("expected existing value to remain, got %q", got)
|
||||
}
|
||||
|
||||
if got := items.Items[4].Value; got != "" {
|
||||
t.Fatalf("expected empty title to remain untouched, got %q", got)
|
||||
}
|
||||
|
||||
if got := items.Items[5].Value; got != "" {
|
||||
t.Fatalf("expected Drafts resolver empty result, got %q", got)
|
||||
}
|
||||
|
||||
if got := items.Items[6].Value; got != "" {
|
||||
t.Fatalf("expected cached remove action to stay empty, got %q", got)
|
||||
}
|
||||
|
||||
if got := items.Items[7].Value; got != "" {
|
||||
t.Fatalf("expected update action to stay empty, got %q", got)
|
||||
}
|
||||
|
||||
items.ResolveValuesByTitle(nil)
|
||||
var nilItems *Items
|
||||
nilItems.ResolveValuesByTitle(func(string, string) string { t.Fatal("should not be called"); return "" })
|
||||
}
|
||||
Reference in New Issue
Block a user