Implement batch approve #489
This commit is contained in:
parent
1ad2d53e16
commit
ef316c98b7
|
@ -173,17 +173,18 @@
|
|||
</div>
|
||||
</div>
|
||||
</v-card-title>
|
||||
<v-card-actions v-if="photo.Quality < 3 && $config.feature('review')">
|
||||
<v-card-actions v-if="photo.Quality < 3 && context === 'review'">
|
||||
<v-layout row wrap align-center>
|
||||
<v-flex xs12>
|
||||
<div class="text-xs-center">
|
||||
<v-btn color="secondary-dark" small flat dark @click.stop="photo.archive()"
|
||||
class="action-approve text-xs-center">
|
||||
class="action-archive text-xs-center">
|
||||
<translate>Archive</translate>
|
||||
</v-btn>
|
||||
<v-btn color="secondary-dark" small depressed dark @click.stop="photo.approve()"
|
||||
class="action-approve text-xs-center">
|
||||
<translate>Approve</translate>
|
||||
<v-icon right dark small>check</v-icon>
|
||||
</v-btn>
|
||||
</div>
|
||||
</v-flex>
|
||||
|
@ -206,6 +207,7 @@ export default {
|
|||
openLocation: Function,
|
||||
album: Object,
|
||||
filter: Object,
|
||||
context: String,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
|
|
@ -25,11 +25,23 @@
|
|||
color="share"
|
||||
@click.stop="dialog.share = true"
|
||||
:disabled="selection.length === 0"
|
||||
v-if="context !== 'archive' && $config.feature('share')"
|
||||
v-if="context !== 'archive' && context !== 'review' && $config.feature('share')"
|
||||
class="action-share"
|
||||
>
|
||||
<v-icon>cloud</v-icon>
|
||||
</v-btn>
|
||||
|
||||
<v-btn
|
||||
fab dark small
|
||||
:title="$gettext('Approve')"
|
||||
color="share"
|
||||
@click.stop="batchApprove"
|
||||
:disabled="selection.length === 0"
|
||||
v-if="context === 'review'"
|
||||
class="action-approve"
|
||||
>
|
||||
<v-icon>check</v-icon>
|
||||
</v-btn>
|
||||
<v-btn
|
||||
fab dark small
|
||||
:title="$gettext('Edit')"
|
||||
|
@ -153,6 +165,13 @@ export default {
|
|||
this.$clipboard.clear();
|
||||
this.expanded = false;
|
||||
},
|
||||
batchApprove() {
|
||||
Api.post("batch/photos/approve", {"photos": this.selection}).then(() => this.onApproved());
|
||||
},
|
||||
onApproved() {
|
||||
Notify.success(this.$gettext("Selection approved"));
|
||||
this.clearClipboard();
|
||||
},
|
||||
batchArchivePhotos() {
|
||||
this.dialog.archive = false;
|
||||
|
||||
|
|
|
@ -116,6 +116,7 @@ export default {
|
|||
openLocation: Function,
|
||||
album: Object,
|
||||
filter: Object,
|
||||
context: String,
|
||||
},
|
||||
data() {
|
||||
let m = this.$gettext("Couldn't find anything.");
|
||||
|
|
|
@ -133,6 +133,7 @@ export default {
|
|||
editPhoto: Function,
|
||||
album: Object,
|
||||
filter: Object,
|
||||
context: String,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
:album="model" context="album"></p-photo-clipboard>
|
||||
|
||||
<p-photo-mosaic v-if="settings.view === 'mosaic'"
|
||||
context="album"
|
||||
:photos="results"
|
||||
:selection="selection"
|
||||
:filter="filter"
|
||||
|
@ -23,6 +24,7 @@
|
|||
:edit-photo="editPhoto"
|
||||
:open-photo="openPhoto"></p-photo-mosaic>
|
||||
<p-photo-list v-else-if="settings.view === 'list'"
|
||||
context="album"
|
||||
:photos="results"
|
||||
:selection="selection"
|
||||
:filter="filter"
|
||||
|
@ -31,6 +33,7 @@
|
|||
:edit-photo="editPhoto"
|
||||
:open-location="openLocation"></p-photo-list>
|
||||
<p-photo-cards v-else
|
||||
context="album"
|
||||
:photos="results"
|
||||
:selection="selection"
|
||||
:filter="filter"
|
||||
|
|
|
@ -14,12 +14,14 @@
|
|||
<p-photo-clipboard :refresh="refresh" :selection="selection" :context="context"></p-photo-clipboard>
|
||||
|
||||
<p-photo-mosaic v-if="settings.view === 'mosaic'"
|
||||
:context="context"
|
||||
:photos="results"
|
||||
:selection="selection"
|
||||
:filter="filter"
|
||||
:edit-photo="editPhoto"
|
||||
:open-photo="openPhoto"></p-photo-mosaic>
|
||||
<p-photo-list v-else-if="settings.view === 'list'"
|
||||
:context="context"
|
||||
:photos="results"
|
||||
:selection="selection"
|
||||
:filter="filter"
|
||||
|
@ -27,6 +29,7 @@
|
|||
:edit-photo="editPhoto"
|
||||
:open-location="openLocation"></p-photo-list>
|
||||
<p-photo-cards v-else
|
||||
:context="context"
|
||||
:photos="results"
|
||||
:selection="selection"
|
||||
:filter="filter"
|
||||
|
@ -129,7 +132,9 @@ export default {
|
|||
return "photos";
|
||||
}
|
||||
|
||||
if (this.staticFilter.archived) {
|
||||
if (this.staticFilter.review) {
|
||||
return "review";
|
||||
} else if (this.staticFilter.archived) {
|
||||
return "archive";
|
||||
} else if (this.staticFilter.favorite) {
|
||||
return "favorites";
|
||||
|
@ -454,8 +459,14 @@ export default {
|
|||
for (let i = 0; i < data.entities.length; i++) {
|
||||
const values = data.entities[i];
|
||||
|
||||
this.updateResult(this.results, values);
|
||||
this.updateResult(this.viewer.results, values);
|
||||
if (this.context === "review" && values.Quality >= 3) {
|
||||
this.removeResult(this.results, values.UID);
|
||||
this.removeResult(this.viewer.results, values.UID);
|
||||
this.$clipboard.removeId(values.UID);
|
||||
} else {
|
||||
this.updateResult(this.results, values);
|
||||
this.updateResult(this.viewer.results, values);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'restored':
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/jinzhu/gorm"
|
||||
"github.com/photoprism/photoprism/internal/acl"
|
||||
|
@ -11,6 +9,7 @@ import (
|
|||
"github.com/photoprism/photoprism/internal/form"
|
||||
"github.com/photoprism/photoprism/internal/i18n"
|
||||
"github.com/photoprism/photoprism/internal/query"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// POST /api/v1/batch/photos/archive
|
||||
|
@ -60,6 +59,56 @@ func BatchPhotosArchive(router *gin.RouterGroup) {
|
|||
})
|
||||
}
|
||||
|
||||
// POST /api/v1/batch/photos/approve
|
||||
func BatchPhotosApprove(router *gin.RouterGroup) {
|
||||
router.POST("batch/photos/approve", func(c *gin.Context) {
|
||||
s := Auth(SessionID(c), acl.ResourcePhotos, acl.ActionUpdate)
|
||||
|
||||
if s.Invalid() {
|
||||
AbortUnauthorized(c)
|
||||
return
|
||||
}
|
||||
|
||||
var f form.Selection
|
||||
|
||||
if err := c.BindJSON(&f); err != nil {
|
||||
AbortBadRequest(c)
|
||||
return
|
||||
}
|
||||
|
||||
if len(f.Photos) == 0 {
|
||||
Abort(c, http.StatusBadRequest, i18n.ErrNoItemsSelected)
|
||||
return
|
||||
}
|
||||
|
||||
log.Infof("photos: approving %s", f.String())
|
||||
|
||||
photos, err := query.PhotoSelection(f)
|
||||
|
||||
if err != nil {
|
||||
AbortEntityNotFound(c)
|
||||
return
|
||||
}
|
||||
|
||||
var approved entity.Photos
|
||||
|
||||
for _, p := range photos {
|
||||
if err := p.Approve(); err != nil {
|
||||
log.Errorf("photo: %s (approve)", err.Error())
|
||||
} else {
|
||||
approved = append(approved, p)
|
||||
SavePhotoAsYaml(p)
|
||||
}
|
||||
}
|
||||
|
||||
UpdateClientConfig()
|
||||
|
||||
event.EntitiesUpdated("photos", approved)
|
||||
|
||||
c.JSON(http.StatusOK, i18n.NewResponse(http.StatusOK, i18n.MsgSelectionApproved))
|
||||
})
|
||||
}
|
||||
|
||||
// POST /api/v1/batch/photos/restore
|
||||
func BatchPhotosRestore(router *gin.RouterGroup) {
|
||||
router.POST("/batch/photos/restore", func(c *gin.Context) {
|
||||
|
|
|
@ -228,11 +228,11 @@ func (m *File) Updates(values interface{}) error {
|
|||
func (m *File) Rename(fileName, rootName, filePath, fileBase string) error {
|
||||
// Update database row.
|
||||
if err := m.Updates(map[string]interface{}{
|
||||
"FileName": fileName,
|
||||
"FileRoot": rootName,
|
||||
"FileName": fileName,
|
||||
"FileRoot": rootName,
|
||||
"FileMissing": false,
|
||||
"DeletedAt": nil,
|
||||
}); err != nil {
|
||||
"DeletedAt": nil,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
@ -263,7 +263,7 @@ func (m *File) Undelete() error {
|
|||
// Update database row.
|
||||
err := m.Updates(map[string]interface{}{
|
||||
"FileMissing": false,
|
||||
"DeletedAt": nil,
|
||||
"DeletedAt": nil,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
|
|
|
@ -60,6 +60,7 @@ const (
|
|||
MsgLabelsDeleted
|
||||
MsgLabelSaved
|
||||
MsgFilesUploadedIn
|
||||
MsgSelectionApproved
|
||||
MsgSelectionArchived
|
||||
MsgSelectionRestored
|
||||
MsgSelectionProtected
|
||||
|
@ -129,6 +130,7 @@ var Messages = MessageMap{
|
|||
MsgLabelsDeleted: gettext("Labels deleted"),
|
||||
MsgLabelSaved: gettext("Label saved"),
|
||||
MsgFilesUploadedIn: gettext("%d files uploaded in %d s"),
|
||||
MsgSelectionApproved: gettext("Selection approved"),
|
||||
MsgSelectionArchived: gettext("Selection archived"),
|
||||
MsgSelectionRestored: gettext("Selection restored"),
|
||||
MsgSelectionProtected: gettext("Selection marked as private"),
|
||||
|
|
|
@ -76,6 +76,7 @@ func registerRoutes(router *gin.Engine, conf *config.Config) {
|
|||
api.StartIndexing(v1)
|
||||
api.CancelIndexing(v1)
|
||||
|
||||
api.BatchPhotosApprove(v1)
|
||||
api.BatchPhotosArchive(v1)
|
||||
api.BatchPhotosRestore(v1)
|
||||
api.BatchPhotosPrivate(v1)
|
||||
|
|
Loading…
Reference in a new issue