From 43714c00d50a4e5f950d43dedb50e3c11951116a Mon Sep 17 00:00:00 2001 From: Michael Mayer Date: Fri, 18 Dec 2020 13:05:48 +0100 Subject: [PATCH] UX: Refactor Library UI --- frontend/src/common/log.js | 2 +- frontend/src/model/account.js | 3 +- frontend/src/pages/library.vue | 105 +++++++++++++++++------- frontend/src/pages/settings.vue | 99 +++++++++++----------- frontend/src/pages/settings/general.vue | 2 +- frontend/src/routes.js | 14 ++-- internal/api/account.go | 44 +++++++++- internal/api/account_test.go | 4 +- internal/api/api_test.go | 25 +++++- internal/api/user.go | 2 +- 10 files changed, 207 insertions(+), 93 deletions(-) diff --git a/frontend/src/common/log.js b/frontend/src/common/log.js index 41c4a67d5..6f2eb9791 100644 --- a/frontend/src/common/log.js +++ b/frontend/src/common/log.js @@ -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 diff --git a/frontend/src/model/account.js b/frontend/src/model/account.js index 7e6ba31cb..c4e84ad2f 100644 --- a/frontend/src/model/account.js +++ b/frontend/src/model/account.js @@ -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: "", diff --git a/frontend/src/pages/library.vue b/frontend/src/pages/library.vue index fe48a5e26..0b31b6d83 100644 --- a/frontend/src/pages/library.vue +++ b/frontend/src/pages/library.vue @@ -8,35 +8,17 @@ slider-color="secondary-dark" :height="$vuetify.breakpoint.smAndDown ? 48 : 64" > - - Index - - - - + + {{ tab.icon }} - - Logs - - - - - - - - - - - - + + @@ -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: { diff --git a/frontend/src/pages/settings.vue b/frontend/src/pages/settings.vue index 0d577c3ca..cbcdd912b 100644 --- a/frontend/src/pages/settings.vue +++ b/frontend/src/pages/settings.vue @@ -10,8 +10,11 @@ :height="$vuetify.breakpoint.smAndDown ? 48 : 64" > - {{ tab.icon }} + @click="changePath(tab.path)"> + {{ tab.icon }} + @@ -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; diff --git a/frontend/src/pages/settings/general.vue b/frontend/src/pages/settings/general.vue index 3ae2c2eaa..0c4c02384 100644 --- a/frontend/src/pages/settings/general.vue +++ b/frontend/src/pages/settings/general.vue @@ -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 > diff --git a/frontend/src/routes.js b/frontend/src/routes.js index f6011612b..af8a8c2e3 100644 --- a/frontend/src/routes.js +++ b/frontend/src/routes.js @@ -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", diff --git a/internal/api/account.go b/internal/api/account.go index 2fc5c9b47..46af3c6bb 100644 --- a/internal/api/account.go +++ b/internal/api/account.go @@ -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) diff --git a/internal/api/account_test.go b/internal/api/account_test.go index 045e3b88e..6941a91ba 100644 --- a/internal/api/account_test.go +++ b/internal/api/account_test.go @@ -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()) diff --git a/internal/api/api_test.go b/internal/api/api_test.go index e527dab90..8f280c55b 100644 --- a/internal/api/api_test.go +++ b/internal/api/api_test.go @@ -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) diff --git a/internal/api/user.go b/internal/api/user.go index 646ded122..3cc6279bc 100644 --- a/internal/api/user.go +++ b/internal/api/user.go @@ -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 }