UX: Refactor Library UI
This commit is contained in:
parent
0925d7179c
commit
43714c00d5
|
@ -32,7 +32,7 @@ import Event from "pubsub-js";
|
|||
|
||||
class Log {
|
||||
constructor() {
|
||||
this.cap = 100;
|
||||
this.cap = 150;
|
||||
this.created = new Date;
|
||||
this.logs = [
|
||||
/* EXAMPLE LOG MESSAGE
|
||||
|
|
|
@ -31,6 +31,7 @@ https://docs.photoprism.org/developer-guide/
|
|||
import RestModel from "model/rest";
|
||||
import Api from "common/api";
|
||||
import {$gettext} from "common/vm";
|
||||
import {config} from "../session";
|
||||
|
||||
export class Account extends RestModel {
|
||||
getDefaults() {
|
||||
|
@ -57,7 +58,7 @@ export class Account extends RestModel {
|
|||
SyncDate: null,
|
||||
SyncFilenames: true,
|
||||
SyncUpload: false,
|
||||
SyncDownload: true,
|
||||
SyncDownload: !config.get("readonly"),
|
||||
SyncRaw: true,
|
||||
CreatedAt: "",
|
||||
UpdatedAt: "",
|
||||
|
|
|
@ -8,35 +8,17 @@
|
|||
slider-color="secondary-dark"
|
||||
:height="$vuetify.breakpoint.smAndDown ? 48 : 64"
|
||||
>
|
||||
<v-tab id="tab-index" ripple @click="changePath('/library')">
|
||||
<translate key="Index">Index</translate>
|
||||
</v-tab>
|
||||
|
||||
<v-tab id="tab-import" :disabled="readonly || !$config.feature('import')" ripple
|
||||
@click="changePath('/library/import')">
|
||||
<template v-if="config.settings.import.move">
|
||||
<translate key="Move">Move</translate>
|
||||
</template>
|
||||
<v-tab v-for="(tab, index) in tabs" :key="index" :id="'tab-' + tab.name" :class="tab.class" ripple
|
||||
@click="changePath(tab.path)">
|
||||
<v-icon v-if="$vuetify.breakpoint.smAndDown" :title="tab.label">{{ tab.icon }}</v-icon>
|
||||
<template v-else>
|
||||
<translate key="Copy">Copy</translate>
|
||||
<v-icon :size="18" left>{{ tab.icon }}</v-icon> {{ tab.label }}
|
||||
</template>
|
||||
</v-tab>
|
||||
|
||||
<v-tab id="tab-logs" ripple @click="changePath('/library/logs')" v-if="$config.feature('logs')">
|
||||
<translate key="Logs">Logs</translate>
|
||||
</v-tab>
|
||||
|
||||
<v-tabs-items touchless>
|
||||
<v-tab-item lazy>
|
||||
<p-tab-index></p-tab-index>
|
||||
</v-tab-item>
|
||||
|
||||
<v-tab-item :disabled="readonly" lazy>
|
||||
<p-tab-import></p-tab-import>
|
||||
</v-tab-item>
|
||||
|
||||
<v-tab-item v-if="$config.feature('logs')">
|
||||
<p-tab-logs></p-tab-logs>
|
||||
<v-tab-item lazy v-for="(tab, index) in tabs" :key="index">
|
||||
<component v-bind:is="tab.component"></component>
|
||||
</v-tab-item>
|
||||
</v-tabs-items>
|
||||
</v-tabs>
|
||||
|
@ -48,10 +30,21 @@ import tabImport from "pages/library/import.vue";
|
|||
import tabIndex from "pages/library/index.vue";
|
||||
import tabLogs from "pages/library/logs.vue";
|
||||
|
||||
function initTabs(flag, tabs) {
|
||||
let i = 0;
|
||||
while(i < tabs.length) {
|
||||
if(!tabs[i][flag]) {
|
||||
tabs.splice(i,1);
|
||||
} else {
|
||||
i++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
name: 'p-page-library',
|
||||
props: {
|
||||
tab: Number
|
||||
tab: String,
|
||||
},
|
||||
components: {
|
||||
'p-tab-index': tabIndex,
|
||||
|
@ -59,10 +52,66 @@ export default {
|
|||
'p-tab-logs': tabLogs,
|
||||
},
|
||||
data() {
|
||||
const config = this.$config.values;
|
||||
const isDemo = this.$config.get("demo");
|
||||
const isPublic = this.$config.get("public");
|
||||
const isReadOnly = this.$config.get("readonly");
|
||||
const canImport = this.$config.feature('import') && !isReadOnly;
|
||||
|
||||
const tabs = [
|
||||
{
|
||||
'name': 'library-index',
|
||||
'component': tabIndex,
|
||||
'label': this.$gettext('Index'),
|
||||
'class': '',
|
||||
'path': '/library',
|
||||
'icon': 'update',
|
||||
'readonly': true,
|
||||
'demo': true,
|
||||
},
|
||||
{
|
||||
'name': 'library-import',
|
||||
'component': tabImport,
|
||||
'label': this.$gettext('Import'),
|
||||
'class': '',
|
||||
'path': '/library/import',
|
||||
'icon': 'perm_media',
|
||||
'readonly': false,
|
||||
'demo': true,
|
||||
},
|
||||
{
|
||||
'name': 'library-logs',
|
||||
'component': tabLogs,
|
||||
'label': this.$gettext('Logs'),
|
||||
'class': '',
|
||||
'path': '/library/logs',
|
||||
'icon': 'notes',
|
||||
'readonly': true,
|
||||
'demo': true,
|
||||
},
|
||||
];
|
||||
|
||||
if(isDemo) {
|
||||
initTabs("demo", tabs);
|
||||
}
|
||||
|
||||
if(!canImport) {
|
||||
initTabs("readonly", tabs);
|
||||
}
|
||||
|
||||
let active = 0;
|
||||
|
||||
if (typeof this.tab === 'string' && this.tab !== '') {
|
||||
active = tabs.findIndex((t) => t.name === this.tab);
|
||||
}
|
||||
|
||||
return {
|
||||
config: this.$config.values,
|
||||
readonly: this.$config.get("readonly"),
|
||||
active: this.tab,
|
||||
tabs: tabs,
|
||||
demo: isDemo,
|
||||
public: isPublic,
|
||||
config: config,
|
||||
readonly: isReadOnly,
|
||||
active: active,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
|
|
|
@ -10,8 +10,11 @@
|
|||
:height="$vuetify.breakpoint.smAndDown ? 48 : 64"
|
||||
>
|
||||
<v-tab v-for="(tab, index) in tabs" :key="index" :id="'tab-' + tab.name" :class="tab.class" ripple
|
||||
@click="changePath(tab.path)" :title="tab.label">
|
||||
<v-icon>{{ tab.icon }}</v-icon>
|
||||
@click="changePath(tab.path)">
|
||||
<v-icon v-if="$vuetify.breakpoint.smAndDown" :title="tab.label">{{ tab.icon }}</v-icon>
|
||||
<template v-else>
|
||||
<v-icon :size="18" left>{{ tab.icon }}</v-icon> {{ tab.label }}
|
||||
</template>
|
||||
</v-tab>
|
||||
|
||||
<v-tabs-items touchless>
|
||||
|
@ -28,52 +31,8 @@ import tabGeneral from "pages/settings/general.vue";
|
|||
import tabLibrary from "pages/settings/library.vue";
|
||||
import tabSync from "pages/settings/sync.vue";
|
||||
import tabAccount from "pages/settings/account.vue";
|
||||
import {$gettext} from "common/vm";
|
||||
|
||||
const tabs = [
|
||||
{
|
||||
'name': 'settings-general',
|
||||
'component': tabGeneral,
|
||||
'label': $gettext('General'),
|
||||
'class': '',
|
||||
'path': '/settings',
|
||||
'icon': 'tv',
|
||||
'public': true,
|
||||
'demo': true,
|
||||
},
|
||||
{
|
||||
'name': 'settings-library',
|
||||
'component': tabLibrary,
|
||||
'label': $gettext('Library'),
|
||||
'class': '',
|
||||
'path': '/settings/library',
|
||||
'icon': 'camera_roll',
|
||||
'public': true,
|
||||
'demo': true,
|
||||
},
|
||||
{
|
||||
'name': 'settings-sync',
|
||||
'component': tabSync,
|
||||
'label': $gettext('Sync'),
|
||||
'class': '',
|
||||
'path': '/settings/sync',
|
||||
'icon': 'sync_alt',
|
||||
'public': false,
|
||||
'demo': true,
|
||||
},
|
||||
{
|
||||
'name': 'settings-account',
|
||||
'component': tabAccount,
|
||||
'label': $gettext('Account'),
|
||||
'class': '',
|
||||
'path': '/settings/account',
|
||||
'icon': 'vpn_key',
|
||||
'public': false,
|
||||
'demo': true,
|
||||
},
|
||||
];
|
||||
|
||||
function initTabs(flag) {
|
||||
function initTabs(flag, tabs) {
|
||||
let i = 0;
|
||||
while(i < tabs.length) {
|
||||
if(!tabs[i][flag]) {
|
||||
|
@ -98,11 +57,53 @@ export default {
|
|||
data() {
|
||||
const isDemo = this.$config.get("demo");
|
||||
const isPublic = this.$config.get("public");
|
||||
const tabs = [
|
||||
{
|
||||
'name': 'settings-general',
|
||||
'component': tabGeneral,
|
||||
'label': this.$gettext('General'),
|
||||
'class': '',
|
||||
'path': '/settings',
|
||||
'icon': 'tv',
|
||||
'public': true,
|
||||
'demo': true,
|
||||
},
|
||||
{
|
||||
'name': 'settings-library',
|
||||
'component': tabLibrary,
|
||||
'label': this.$gettext('Library'),
|
||||
'class': '',
|
||||
'path': '/settings/library',
|
||||
'icon': 'camera_roll',
|
||||
'public': true,
|
||||
'demo': true,
|
||||
},
|
||||
{
|
||||
'name': 'settings-sync',
|
||||
'component': tabSync,
|
||||
'label': this.$gettext('Sync'),
|
||||
'class': '',
|
||||
'path': '/settings/sync',
|
||||
'icon': 'sync_alt',
|
||||
'public': true,
|
||||
'demo': true,
|
||||
},
|
||||
{
|
||||
'name': 'settings-account',
|
||||
'component': tabAccount,
|
||||
'label': this.$gettext('Account'),
|
||||
'class': '',
|
||||
'path': '/settings/account',
|
||||
'icon': 'person',
|
||||
'public': false,
|
||||
'demo': true,
|
||||
},
|
||||
];
|
||||
|
||||
if(isDemo) {
|
||||
initTabs("demo");
|
||||
initTabs("demo", tabs);
|
||||
} else if(isPublic) {
|
||||
initTabs("public");
|
||||
initTabs("public", tabs);
|
||||
}
|
||||
|
||||
let active = 0;
|
||||
|
|
|
@ -190,7 +190,7 @@
|
|||
color="secondary-dark"
|
||||
:label="$gettext('Import')"
|
||||
:hint="$gettext('Imported files will be sorted by date and given a unique name.')"
|
||||
prepend-icon="create_new_folder"
|
||||
prepend-icon="perm_media"
|
||||
persistent-hint
|
||||
>
|
||||
</v-checkbox>
|
||||
|
|
|
@ -249,25 +249,25 @@ export default [
|
|||
meta: {title: $gettext("People"), auth: true},
|
||||
},
|
||||
{
|
||||
name: "library_logs",
|
||||
path: "/library/logs",
|
||||
name: "library",
|
||||
path: "/library",
|
||||
component: Library,
|
||||
meta: {title: $gettext("Library"), auth: true, background: "application-light"},
|
||||
props: {tab: 2},
|
||||
props: {tab: "library-index"},
|
||||
},
|
||||
{
|
||||
name: "library_import",
|
||||
path: "/library/import",
|
||||
component: Library,
|
||||
meta: {title: $gettext("Library"), auth: true, background: "application-light"},
|
||||
props: {tab: 1},
|
||||
props: {tab: "library-import"},
|
||||
},
|
||||
{
|
||||
name: "library",
|
||||
path: "/library",
|
||||
name: "library_logs",
|
||||
path: "/library/logs",
|
||||
component: Library,
|
||||
meta: {title: $gettext("Library"), auth: true, background: "application-light"},
|
||||
props: {tab: 0},
|
||||
props: {tab: "library-logs"},
|
||||
},
|
||||
{
|
||||
name: "settings",
|
||||
|
|
|
@ -31,6 +31,13 @@ func GetAccounts(router *gin.RouterGroup) {
|
|||
return
|
||||
}
|
||||
|
||||
conf := service.Config()
|
||||
|
||||
if conf.Demo() || conf.DisableSettings() {
|
||||
c.JSON(http.StatusOK, entity.Accounts{})
|
||||
return
|
||||
}
|
||||
|
||||
var f form.AccountSearch
|
||||
|
||||
err := c.MustBindWith(&f, binding.Form)
|
||||
|
@ -68,6 +75,13 @@ func GetAccount(router *gin.RouterGroup) {
|
|||
return
|
||||
}
|
||||
|
||||
conf := service.Config()
|
||||
|
||||
if conf.Demo() || conf.DisableSettings() {
|
||||
AbortUnauthorized(c)
|
||||
return
|
||||
}
|
||||
|
||||
id := ParseUint(c.Param("id"))
|
||||
|
||||
if m, err := query.AccountByID(id); err == nil {
|
||||
|
@ -91,6 +105,13 @@ func GetAccountFolders(router *gin.RouterGroup) {
|
|||
return
|
||||
}
|
||||
|
||||
conf := service.Config()
|
||||
|
||||
if conf.Demo() || conf.DisableSettings() {
|
||||
AbortUnauthorized(c)
|
||||
return
|
||||
}
|
||||
|
||||
start := time.Now()
|
||||
id := ParseUint(c.Param("id"))
|
||||
cache := service.Cache()
|
||||
|
@ -191,6 +212,13 @@ func CreateAccount(router *gin.RouterGroup) {
|
|||
return
|
||||
}
|
||||
|
||||
conf := service.Config()
|
||||
|
||||
if conf.Demo() || conf.DisableSettings() {
|
||||
AbortUnauthorized(c)
|
||||
return
|
||||
}
|
||||
|
||||
var f form.Account
|
||||
|
||||
if err := c.BindJSON(&f); err != nil {
|
||||
|
@ -206,8 +234,6 @@ func CreateAccount(router *gin.RouterGroup) {
|
|||
|
||||
m, err := entity.CreateAccount(f)
|
||||
|
||||
log.Debugf("account: creating %+v %+v", f, m)
|
||||
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
AbortBadRequest(c)
|
||||
|
@ -233,6 +259,13 @@ func UpdateAccount(router *gin.RouterGroup) {
|
|||
return
|
||||
}
|
||||
|
||||
conf := service.Config()
|
||||
|
||||
if conf.Demo() || conf.DisableSettings() {
|
||||
AbortUnauthorized(c)
|
||||
return
|
||||
}
|
||||
|
||||
id := ParseUint(c.Param("id"))
|
||||
|
||||
m, err := query.AccountByID(id)
|
||||
|
@ -295,6 +328,13 @@ func DeleteAccount(router *gin.RouterGroup) {
|
|||
return
|
||||
}
|
||||
|
||||
conf := service.Config()
|
||||
|
||||
if conf.Demo() || conf.DisableSettings() {
|
||||
AbortUnauthorized(c)
|
||||
return
|
||||
}
|
||||
|
||||
id := ParseUint(c.Param("id"))
|
||||
|
||||
m, err := query.AccountByID(id)
|
||||
|
|
|
@ -12,9 +12,9 @@ import (
|
|||
|
||||
func TestGetAccounts(t *testing.T) {
|
||||
t.Run("successful request", func(t *testing.T) {
|
||||
app, router, _ := NewApiTest()
|
||||
app, router, _, sess := NewAdminApiTest()
|
||||
GetAccounts(router)
|
||||
r := PerformRequest(app, "GET", "/api/v1/accounts?count=10")
|
||||
r := AuthenticatedRequest(app, "GET", "/api/v1/accounts?count=10", sess)
|
||||
val := gjson.Get(r.Body.String(), "#(AccName=\"Test Account\").AccURL")
|
||||
count := gjson.Get(r.Body.String(), "#")
|
||||
assert.LessOrEqual(t, int64(1), count.Int())
|
||||
|
|
|
@ -13,7 +13,7 @@ import (
|
|||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// NewApiTest returns new API test helper
|
||||
// NewApiTest returns new API test helper.
|
||||
func NewApiTest() (app *gin.Engine, router *gin.RouterGroup, conf *config.Config) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
app = gin.New()
|
||||
|
@ -21,6 +21,20 @@ func NewApiTest() (app *gin.Engine, router *gin.RouterGroup, conf *config.Config
|
|||
return app, router, service.Config()
|
||||
}
|
||||
|
||||
// NewApiTest returns new API test helper with authenticated admin session.
|
||||
func NewAdminApiTest() (app *gin.Engine, router *gin.RouterGroup, conf *config.Config, sessId string) {
|
||||
app = gin.New()
|
||||
router = app.Group("/api/v1")
|
||||
CreateSession(router)
|
||||
reader := strings.NewReader(`{"username": "admin", "password": "photoprism"}`)
|
||||
req, _ := http.NewRequest("POST", "/api/v1/session", reader)
|
||||
w := httptest.NewRecorder()
|
||||
app.ServeHTTP(w, req)
|
||||
sessId = w.Header().Get("X-Session-ID")
|
||||
gin.SetMode(gin.TestMode)
|
||||
return app, router, service.Config(), sessId
|
||||
}
|
||||
|
||||
// Performs API request with empty request body.
|
||||
// See https://medium.com/@craigchilds94/testing-gin-json-responses-1f258ce3b0b1
|
||||
func PerformRequest(r http.Handler, method, path string) *httptest.ResponseRecorder {
|
||||
|
@ -30,6 +44,15 @@ func PerformRequest(r http.Handler, method, path string) *httptest.ResponseRecor
|
|||
return w
|
||||
}
|
||||
|
||||
// Performs authenticated API request with empty request body.
|
||||
func AuthenticatedRequest(r http.Handler, method, path, sess string) *httptest.ResponseRecorder {
|
||||
req, _ := http.NewRequest(method, path, nil)
|
||||
req.Header.Add("X-Session-ID", sess)
|
||||
w := httptest.NewRecorder()
|
||||
r.ServeHTTP(w, req)
|
||||
return w
|
||||
}
|
||||
|
||||
// Performs API request including request body as string.
|
||||
func PerformRequestWithBody(r http.Handler, method, path, body string) *httptest.ResponseRecorder {
|
||||
reader := strings.NewReader(body)
|
||||
|
|
|
@ -16,7 +16,7 @@ func ChangePassword(router *gin.RouterGroup) {
|
|||
router.PUT("/users/:uid/password", func(c *gin.Context) {
|
||||
conf := service.Config()
|
||||
|
||||
if conf.Public() {
|
||||
if conf.Public() || conf.DisableSettings() {
|
||||
Abort(c, http.StatusForbidden, i18n.ErrPublic)
|
||||
return
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue