mirror of
https://github.com/photoprism/photoprism.git
synced 2025-12-12 00:34:13 +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"
|
"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.
|
// String represents batch edit form value.
|
||||||
type String struct {
|
type String struct {
|
||||||
Value string `json:"value"`
|
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