From ad3da85ecb22f952383fb01772f217532cce94a3 Mon Sep 17 00:00:00 2001 From: Michael Mayer Date: Sun, 23 Jul 2023 17:57:48 +0200 Subject: [PATCH] UX: Add Delete All button to archive page toolbar #272 Signed-off-by: Michael Mayer --- frontend/src/component/photo/clipboard.vue | 8 ++-- frontend/src/component/photo/toolbar.vue | 44 +++++++++++++++++++++- frontend/src/page/photos.vue | 4 +- internal/api/batch.go | 38 ++++++++++++++----- internal/form/selection.go | 9 ++++- internal/form/selection_test.go | 4 +- internal/photoprism/purge.go | 2 +- internal/query/photo.go | 19 ++++++++-- internal/query/photo_test.go | 18 ++++++++- 9 files changed, 119 insertions(+), 27 deletions(-) diff --git a/frontend/src/component/photo/clipboard.vue b/frontend/src/component/photo/clipboard.vue index 1cdfed2d4..287b56188 100644 --- a/frontend/src/component/photo/clipboard.vue +++ b/frontend/src/component/photo/clipboard.vue @@ -163,6 +163,10 @@ import Photo from "model/photo"; export default { name: 'PPhotoClipboard', props: { + context: { + type: String, + default: 'photos', + }, selection: { type: Array, default: () => [], @@ -175,10 +179,6 @@ export default { type: Object, default: () => {}, }, - context: { - type: String, - default: '', - }, }, data() { const features = this.$config.settings().features; diff --git a/frontend/src/component/photo/toolbar.vue b/frontend/src/component/photo/toolbar.vue index e54c9b0db..3a3f767b1 100644 --- a/frontend/src/component/photo/toolbar.vue +++ b/frontend/src/component/photo/toolbar.vue @@ -32,7 +32,11 @@ view_column - + delete + + cloud_upload @@ -166,15 +170,23 @@ + diff --git a/frontend/src/page/photos.vue b/frontend/src/page/photos.vue index 74919981a..60dffebaa 100644 --- a/frontend/src/page/photos.vue +++ b/frontend/src/page/photos.vue @@ -3,7 +3,7 @@ :infinite-scroll-disabled="scrollDisabled" :infinite-scroll-distance="scrollDistance" :infinite-scroll-listen-for-event="'scrollRefresh'"> - @@ -12,7 +12,7 @@ - + 0 { + log.Infof("archive: deleting %s", english.Plural(len(photos), "photo", "photos")) + } else { + Abort(c, http.StatusBadRequest, i18n.ErrNoItemsSelected) return } @@ -398,8 +416,8 @@ func BatchPhotosDelete(router *gin.RouterGroup) { } } - if numFiles > 0 { - log.Infof("delete: removed %s [%s]", english.Plural(numFiles, "file", "files"), time.Since(deleteStart)) + if numFiles > 0 || len(deleted) > 0 { + log.Infof("archive: deleted %s and %s [%s]", english.Plural(numFiles, "file", "files"), english.Plural(len(deleted), "photo", "photos"), time.Since(deleteStart)) } // Any photos deleted? diff --git a/internal/form/selection.go b/internal/form/selection.go index 92ef810ba..360b31672 100644 --- a/internal/form/selection.go +++ b/internal/form/selection.go @@ -2,7 +2,9 @@ package form import "strings" +// Selection represents items selected in the user interface. type Selection struct { + All bool `json:"all"` Files []string `json:"files"` Photos []string `json:"photos"` Albums []string `json:"albums"` @@ -11,6 +13,7 @@ type Selection struct { Subjects []string `json:"subjects"` } +// Empty checks if any specific items were selected. func (f Selection) Empty() bool { switch { case len(f.Files) > 0: @@ -30,7 +33,8 @@ func (f Selection) Empty() bool { return true } -func (f Selection) All() []string { +// Get returns a string slice with the selected item UIDs. +func (f Selection) Get() []string { var all []string copy(all, f.Files) @@ -44,6 +48,7 @@ func (f Selection) All() []string { return all } +// String returns a string containing all selected item UIDs. func (f Selection) String() string { - return strings.Join(f.All(), ", ") + return strings.Join(f.Get(), ", ") } diff --git a/internal/form/selection_test.go b/internal/form/selection_test.go index 59d190505..16284b9e6 100644 --- a/internal/form/selection_test.go +++ b/internal/form/selection_test.go @@ -38,10 +38,10 @@ func TestSelection_Empty(t *testing.T) { }) } -func TestSelection_All(t *testing.T) { +func TestSelection_Get(t *testing.T) { t.Run("success", func(t *testing.T) { sel := Selection{Photos: []string{"p123", "p456"}, Albums: []string{"a123"}, Labels: []string{"l123", "l456", "l789"}, Files: []string{"f567", "f111"}, Places: []string{"p568"}, Subjects: []string{"jqzkpo13j8ngpgv4"}} - assert.Equal(t, []string{"p123", "p456", "a123", "l123", "l456", "l789", "p568", "jqzkpo13j8ngpgv4"}, sel.All()) + assert.Equal(t, []string{"p123", "p456", "a123", "l123", "l456", "l789", "p568", "jqzkpo13j8ngpgv4"}, sel.Get()) }) } diff --git a/internal/photoprism/purge.go b/internal/photoprism/purge.go index 0c62c467b..ff66e31c1 100644 --- a/internal/photoprism/purge.go +++ b/internal/photoprism/purge.go @@ -215,7 +215,7 @@ func (w *Purge) Start(opt PurgeOptions) (purgedFiles map[string]bool, purgedPhot limit = 10000 offset = 0 for { - photos, err := query.PhotosMissing(limit, offset) + photos, err := query.MissingPhotos(limit, offset) if err != nil { return purgedFiles, purgedPhotos, updates(), err diff --git a/internal/query/photo.go b/internal/query/photo.go index 3281616a3..81c9253a4 100644 --- a/internal/query/photo.go +++ b/internal/query/photo.go @@ -72,13 +72,26 @@ func PhotoPreloadByUID(photoUID string) (photo entity.Photo, err error) { return photo, nil } -// PhotosMissing returns photo entities without existing files. -func PhotosMissing(limit int, offset int) (entities entity.Photos, err error) { +// MissingPhotos returns photo entities without existing files. +func MissingPhotos(limit int, offset int) (entities entity.Photos, err error) { err = Db(). Select("photos.*"). Where("id NOT IN (SELECT photo_id FROM files WHERE file_missing = 0 AND file_root = '/' AND deleted_at IS NULL)"). Where("photos.photo_type <> ?", entity.MediaText). - Group("photos.id"). + Order("photos.id"). + Limit(limit).Offset(offset).Find(&entities).Error + + return entities, err +} + +// ArchivedPhotos finds and returns archived photos. +func ArchivedPhotos(limit int, offset int) (entities entity.Photos, err error) { + err = UnscopedDb(). + Select("photos.*"). + Where("photos.photo_quality > -1"). + Where("photos.deleted_at IS NOT NULL"). + Where("photos.photo_type <> ?", entity.MediaText). + Order("photos.id"). Limit(limit).Offset(offset).Find(&entities).Error return entities, err diff --git a/internal/query/photo_test.go b/internal/query/photo_test.go index dd6363f42..e7bac2ee4 100644 --- a/internal/query/photo_test.go +++ b/internal/query/photo_test.go @@ -58,7 +58,7 @@ func TestPreloadPhotoByUID(t *testing.T) { } func TestMissingPhotos(t *testing.T) { - result, err := PhotosMissing(15, 0) + result, err := MissingPhotos(15, 0) if err != nil { t.Fatal(err) @@ -67,6 +67,22 @@ func TestMissingPhotos(t *testing.T) { assert.LessOrEqual(t, 1, len(result)) } +func TestArchivedPhotos(t *testing.T) { + results, err := ArchivedPhotos(15, 0) + + if err != nil { + t.Fatal(err) + } + + assert.Equal(t, 1, len(results)) + + if len(results) > 1 { + result := results[0] + assert.Equal(t, "image", result.PhotoType) + assert.Equal(t, "pt9jtdre2lvl0y25", result.PhotoUID) + } +} + func TestPhotosMetadataUpdate(t *testing.T) { interval := entity.MetadataUpdateInterval result, err := PhotosMetadataUpdate(10, 0, time.Second, interval)