mirror of
https://github.com/photoprism/photoprism.git
synced 2026-01-23 02:24:24 +00:00
Signed-off-by: Michael Mayer <michael@photoprism.app>
This commit is contained in:
parent
49653d24bb
commit
1a3fdcdad4
11 changed files with 157 additions and 2 deletions
|
|
@ -215,6 +215,7 @@ export default {
|
|||
},
|
||||
data() {
|
||||
const features = this.$config.getSettings().features;
|
||||
const canEdit = this.$config.allow("photos", "update") && features.edit;
|
||||
|
||||
return {
|
||||
selection: this.$clipboard.selection,
|
||||
|
|
@ -225,9 +226,9 @@ export default {
|
|||
canShare: this.$config.allow("photos", "share") && features.share,
|
||||
canServiceUpload: this.$config.feature("services") && this.$config.allow("services", "upload"),
|
||||
canManage: this.$config.allow("photos", "manage") && features.albums,
|
||||
canEdit: this.$config.allow("photos", "update") && features.edit,
|
||||
canEdit: canEdit,
|
||||
canBatchEdit: canEdit && this.$config.allow("photos", "access_all") && features.batchEdit,
|
||||
canEditAlbum: this.$config.allow("albums", "update") && features.albums,
|
||||
canBatchEdit: this.$config.allow("photos", "update") && this.$config.allow("photos", "access_all"),
|
||||
busy: false,
|
||||
config: this.$config.values,
|
||||
expanded: false,
|
||||
|
|
@ -425,6 +426,11 @@ export default {
|
|||
download(path, "photos.zip");
|
||||
},
|
||||
edit() {
|
||||
if (!this.canEdit) {
|
||||
$notify.error(this.$gettext("Disabled"));
|
||||
return;
|
||||
}
|
||||
|
||||
// Open Edit or Batch Edit Dialog.
|
||||
if (!this.canBatchEdit || this.selection.length === 1) {
|
||||
this.$event.PubSub.publish("dialog.edit", { selection: this.selection, album: this.album, index: 0 });
|
||||
|
|
|
|||
|
|
@ -229,6 +229,21 @@
|
|||
</v-checkbox>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12" sm="6" lg="3" class="px-2 pb-2 pt-2">
|
||||
<v-checkbox
|
||||
v-model="settings.features.batchEdit"
|
||||
:disabled="busy || isDemo || !settings.features.edit"
|
||||
class="ma-0 pa-0 input-batch-edit"
|
||||
density="compact"
|
||||
:label="$gettext('Batch Edit')"
|
||||
:hint="$gettext('Edit the metadata, labels, and albums of multiple pictures at once.')"
|
||||
prepend-icon="mdi-form-select"
|
||||
persistent-hint
|
||||
@update:model-value="onChange"
|
||||
>
|
||||
</v-checkbox>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12" sm="6" lg="3" class="px-2 pb-2 pt-2">
|
||||
<v-checkbox
|
||||
v-model="settings.features.archive"
|
||||
|
|
|
|||
104
frontend/tests/vitest/component/photo/clipboard.test.js
Normal file
104
frontend/tests/vitest/component/photo/clipboard.test.js
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
import { describe, it, expect, vi } from "vitest";
|
||||
import { shallowMount } from "@vue/test-utils";
|
||||
import PPhotoClipboard from "component/photo/clipboard.vue";
|
||||
|
||||
const baseFeatures = {
|
||||
edit: true,
|
||||
batchEdit: true,
|
||||
private: true,
|
||||
archive: true,
|
||||
delete: true,
|
||||
download: true,
|
||||
share: true,
|
||||
albums: true,
|
||||
};
|
||||
|
||||
function mountClipboard({ featureOverrides = {}, allowAccessAll = true } = {}) {
|
||||
const publish = vi.fn();
|
||||
const clipboard = {
|
||||
selection: ["pt5y3865st5p3k5l", "pt5y3863oyip9a2d"],
|
||||
clear: vi.fn(),
|
||||
};
|
||||
|
||||
const features = { ...baseFeatures, ...featureOverrides };
|
||||
|
||||
const allowMock = vi.fn((resource, action) => {
|
||||
if (resource === "photos" && action === "access_all") {
|
||||
return allowAccessAll;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
const wrapper = shallowMount(PPhotoClipboard, {
|
||||
global: {
|
||||
mocks: {
|
||||
$config: {
|
||||
getSettings: () => ({ features }),
|
||||
allow: allowMock,
|
||||
feature: vi.fn().mockReturnValue(true),
|
||||
values: {},
|
||||
},
|
||||
$clipboard: clipboard,
|
||||
$notify: {
|
||||
success: vi.fn(),
|
||||
error: vi.fn(),
|
||||
},
|
||||
$event: {
|
||||
PubSub: { publish },
|
||||
},
|
||||
$gettext: (msg) => msg,
|
||||
$pgettext: (_ctx, msg) => msg,
|
||||
$isRtl: false,
|
||||
},
|
||||
stubs: {
|
||||
"v-speed-dial": { template: "<div><slot></slot></div>" },
|
||||
"v-btn": { template: "<button><slot></slot></button>" },
|
||||
"v-icon": { template: "<i></i>" },
|
||||
"p-photo-archive-dialog": true,
|
||||
"p-confirm-dialog": true,
|
||||
"p-photo-album-dialog": true,
|
||||
"p-service-upload": true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return { wrapper, publish, clipboard };
|
||||
}
|
||||
|
||||
describe("component/photo/clipboard", () => {
|
||||
it("publishes dialog.batchedit when the feature flag is enabled and multiple photos are selected", () => {
|
||||
const { wrapper, publish, clipboard } = mountClipboard();
|
||||
|
||||
wrapper.vm.edit();
|
||||
|
||||
expect(publish).toHaveBeenCalledWith("dialog.batchedit", {
|
||||
selection: clipboard.selection,
|
||||
album: wrapper.vm.album,
|
||||
index: 0,
|
||||
});
|
||||
});
|
||||
|
||||
it("falls back to dialog.edit when the batchEdit flag is disabled", () => {
|
||||
const { wrapper, publish, clipboard } = mountClipboard({ featureOverrides: { batchEdit: false } });
|
||||
|
||||
wrapper.vm.edit();
|
||||
|
||||
expect(publish).toHaveBeenCalledWith("dialog.edit", {
|
||||
selection: clipboard.selection,
|
||||
album: wrapper.vm.album,
|
||||
index: 0,
|
||||
});
|
||||
});
|
||||
|
||||
it("does not allow batch edit when access_all permission is missing", () => {
|
||||
const { wrapper, publish, clipboard } = mountClipboard({ allowAccessAll: false });
|
||||
|
||||
wrapper.vm.edit();
|
||||
|
||||
expect(publish).toHaveBeenCalledWith("dialog.edit", {
|
||||
selection: clipboard.selection,
|
||||
album: wrapper.vm.album,
|
||||
index: 0,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -136,6 +136,7 @@ func TestConfig_ClientRoleConfig(t *testing.T) {
|
|||
Delete: true,
|
||||
Download: true,
|
||||
Edit: true,
|
||||
BatchEdit: true,
|
||||
Estimates: true,
|
||||
Favorites: true,
|
||||
Files: true,
|
||||
|
|
@ -175,6 +176,7 @@ func TestConfig_ClientRoleConfig(t *testing.T) {
|
|||
Delete: false,
|
||||
Download: true,
|
||||
Edit: false,
|
||||
BatchEdit: false,
|
||||
Estimates: true,
|
||||
Favorites: false,
|
||||
Files: false,
|
||||
|
|
@ -214,6 +216,7 @@ func TestConfig_ClientRoleConfig(t *testing.T) {
|
|||
Delete: false,
|
||||
Download: true,
|
||||
Edit: false,
|
||||
BatchEdit: false,
|
||||
Estimates: true,
|
||||
Favorites: false,
|
||||
Files: false,
|
||||
|
|
@ -254,6 +257,7 @@ func TestConfig_ClientRoleConfig(t *testing.T) {
|
|||
assert.False(t, f.People)
|
||||
assert.False(t, f.Settings)
|
||||
assert.False(t, f.Edit)
|
||||
assert.False(t, f.BatchEdit)
|
||||
assert.False(t, f.Private)
|
||||
assert.False(t, f.Upload)
|
||||
assert.False(t, f.Download)
|
||||
|
|
@ -297,6 +301,7 @@ func TestConfig_ClientSessionConfig(t *testing.T) {
|
|||
assert.True(t, f.People)
|
||||
assert.True(t, f.Settings)
|
||||
assert.True(t, f.Edit)
|
||||
assert.True(t, f.BatchEdit)
|
||||
assert.True(t, f.Private)
|
||||
assert.True(t, f.Upload)
|
||||
assert.True(t, f.Download)
|
||||
|
|
@ -328,6 +333,7 @@ func TestConfig_ClientSessionConfig(t *testing.T) {
|
|||
assert.True(t, f.People)
|
||||
assert.True(t, f.Settings)
|
||||
assert.True(t, f.Edit)
|
||||
assert.True(t, f.BatchEdit)
|
||||
assert.True(t, f.Private)
|
||||
assert.True(t, f.Upload)
|
||||
assert.True(t, f.Download)
|
||||
|
|
@ -358,6 +364,7 @@ func TestConfig_ClientSessionConfig(t *testing.T) {
|
|||
assert.False(t, f.People)
|
||||
assert.False(t, f.Settings)
|
||||
assert.True(t, f.Edit)
|
||||
assert.True(t, f.BatchEdit)
|
||||
assert.True(t, f.Private)
|
||||
assert.True(t, f.Upload)
|
||||
assert.True(t, f.Download)
|
||||
|
|
@ -390,6 +397,7 @@ func TestConfig_ClientSessionConfig(t *testing.T) {
|
|||
assert.False(t, f.People)
|
||||
assert.False(t, f.Settings)
|
||||
assert.False(t, f.Edit)
|
||||
assert.False(t, f.BatchEdit)
|
||||
assert.False(t, f.Private)
|
||||
assert.False(t, f.Upload)
|
||||
assert.True(t, f.Download)
|
||||
|
|
@ -454,6 +462,7 @@ func TestConfig_ClientSessionConfig(t *testing.T) {
|
|||
assert.False(t, f.People)
|
||||
assert.False(t, f.Settings)
|
||||
assert.False(t, f.Edit)
|
||||
assert.False(t, f.BatchEdit)
|
||||
assert.False(t, f.Private)
|
||||
assert.False(t, f.Upload)
|
||||
assert.False(t, f.Download)
|
||||
|
|
@ -483,6 +492,7 @@ func TestConfig_ClientSessionConfig(t *testing.T) {
|
|||
assert.True(t, f.People)
|
||||
assert.True(t, f.Settings)
|
||||
assert.True(t, f.Edit)
|
||||
assert.True(t, f.BatchEdit)
|
||||
assert.True(t, f.Private)
|
||||
assert.True(t, f.Upload)
|
||||
assert.True(t, f.Download)
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ func (s *Settings) ApplyACL(list acl.ACL, role acl.Role) *Settings {
|
|||
m.Features.Archive = s.Features.Archive && list.AllowAny(acl.ResourcePhotos, role, acl.Permissions{acl.ActionDelete})
|
||||
m.Features.Delete = s.Features.Delete && list.AllowAny(acl.ResourcePhotos, role, acl.Permissions{acl.ActionDelete})
|
||||
m.Features.Edit = s.Features.Edit && list.AllowAny(acl.ResourcePhotos, role, acl.Permissions{acl.ActionUpdate})
|
||||
m.Features.BatchEdit = s.Features.BatchEdit && s.Features.Edit && list.AllowAll(acl.ResourcePhotos, role, acl.Permissions{acl.AccessAll, acl.ActionUpdate})
|
||||
m.Features.Review = s.Features.Review
|
||||
m.Features.Share = s.Features.Share && list.AllowAny(acl.ResourceShares, role, acl.Permissions{acl.ActionManage})
|
||||
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ func TestSettings_ApplyACL(t *testing.T) {
|
|||
Delete: true,
|
||||
Download: true,
|
||||
Edit: true,
|
||||
BatchEdit: true,
|
||||
Estimates: true,
|
||||
Favorites: true,
|
||||
Files: true,
|
||||
|
|
@ -61,6 +62,7 @@ func TestSettings_ApplyACL(t *testing.T) {
|
|||
Delete: false,
|
||||
Download: true,
|
||||
Edit: false,
|
||||
BatchEdit: false,
|
||||
Estimates: true,
|
||||
Favorites: false,
|
||||
Files: false,
|
||||
|
|
@ -90,4 +92,12 @@ func TestSettings_ApplyACL(t *testing.T) {
|
|||
t.Logf("RoleVisitor: %#v", r)
|
||||
assert.Equal(t, expected, r.Features)
|
||||
})
|
||||
|
||||
t.Run("RoleClient", func(t *testing.T) {
|
||||
s := NewDefaultSettings()
|
||||
|
||||
r := s.ApplyACL(acl.Rules, acl.RoleClient)
|
||||
|
||||
assert.True(t, r.Features.BatchEdit)
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ type FeatureSettings struct {
|
|||
Delete bool `json:"delete" yaml:"Delete"`
|
||||
Download bool `json:"download" yaml:"Download"`
|
||||
Edit bool `json:"edit" yaml:"Edit"`
|
||||
BatchEdit bool `json:"batchEdit" yaml:"BatchEdit"`
|
||||
Estimates bool `json:"estimates" yaml:"Estimates"`
|
||||
Favorites bool `json:"favorites" yaml:"Favorites"`
|
||||
Files bool `json:"files" yaml:"Files"`
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ func (s *Settings) ApplyScope(scope string) *Settings {
|
|||
m.Features.Archive = s.Features.Archive && scopes.Contains(acl.ResourcePhotos.String())
|
||||
m.Features.Delete = s.Features.Delete && scopes.Contains(acl.ResourcePhotos.String())
|
||||
m.Features.Edit = s.Features.Edit && scopes.Contains(acl.ResourcePhotos.String())
|
||||
m.Features.BatchEdit = s.Features.BatchEdit && s.Features.Edit && scopes.Contains(acl.ResourcePhotos.String())
|
||||
m.Features.Share = s.Features.Share && scopes.Contains(acl.ResourceShares.String())
|
||||
|
||||
// Browse, upload and download files.
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ func TestSettings_ApplyScope(t *testing.T) {
|
|||
Delete: true,
|
||||
Download: true,
|
||||
Edit: true,
|
||||
BatchEdit: true,
|
||||
Estimates: true,
|
||||
Favorites: true,
|
||||
Files: true,
|
||||
|
|
@ -65,6 +66,7 @@ func TestSettings_ApplyScope(t *testing.T) {
|
|||
Delete: true,
|
||||
Download: true,
|
||||
Edit: true,
|
||||
BatchEdit: true,
|
||||
Estimates: true,
|
||||
Favorites: false,
|
||||
Files: false,
|
||||
|
|
@ -105,6 +107,7 @@ func TestSettings_ApplyScope(t *testing.T) {
|
|||
Delete: false,
|
||||
Download: true,
|
||||
Edit: false,
|
||||
BatchEdit: false,
|
||||
Estimates: true,
|
||||
Favorites: false,
|
||||
Files: false,
|
||||
|
|
@ -144,6 +147,7 @@ func TestSettings_ApplyScope(t *testing.T) {
|
|||
Delete: false,
|
||||
Download: true,
|
||||
Edit: false,
|
||||
BatchEdit: false,
|
||||
Estimates: true,
|
||||
Favorites: false,
|
||||
Files: false,
|
||||
|
|
@ -183,6 +187,7 @@ func TestSettings_ApplyScope(t *testing.T) {
|
|||
Delete: false,
|
||||
Download: false,
|
||||
Edit: false,
|
||||
BatchEdit: false,
|
||||
Estimates: true,
|
||||
Favorites: false,
|
||||
Files: false,
|
||||
|
|
|
|||
|
|
@ -86,6 +86,7 @@ func NewSettings(theme, language, timeZone string) *Settings {
|
|||
Labels: true,
|
||||
Places: true,
|
||||
Edit: true,
|
||||
BatchEdit: true,
|
||||
Archive: true,
|
||||
Review: true,
|
||||
Share: true,
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ Features:
|
|||
Delete: true
|
||||
Download: true
|
||||
Edit: true
|
||||
BatchEdit: true
|
||||
Estimates: true
|
||||
Favorites: true
|
||||
Files: true
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue