diff --git a/api/funkwhale_api/common/dynamic_preferences_registry.py b/api/funkwhale_api/common/dynamic_preferences_registry.py index 2374de7c79fe841ae63d8c18354335cf6fd022b2..15b182671bbf386b401eeb541c3839889342e29d 100644 --- a/api/funkwhale_api/common/dynamic_preferences_registry.py +++ b/api/funkwhale_api/common/dynamic_preferences_registry.py @@ -16,5 +16,5 @@ class APIAutenticationRequired( help_text = ( 'If disabled, anonymous users will be able to query the API' 'and access music data (as well as other data exposed in the API ' - 'without specific permissions)' + 'without specific permissions).' ) diff --git a/api/funkwhale_api/federation/dynamic_preferences_registry.py b/api/funkwhale_api/federation/dynamic_preferences_registry.py index e86b9f6f2b8b30da1c82145ef288e591449aa9f3..8b1b2b03f9eb00f0eac1d467c099baf66d72d67d 100644 --- a/api/funkwhale_api/federation/dynamic_preferences_registry.py +++ b/api/funkwhale_api/federation/dynamic_preferences_registry.py @@ -19,6 +19,9 @@ class MusicCacheDuration(types.IntPreference): 'locally? Federated files that were not listened in this interval ' 'will be erased and refetched from the remote on the next listening.' ) + field_kwargs = { + 'required': False, + } @global_preferences_registry.register @@ -29,7 +32,7 @@ class Enabled(preferences.DefaultFromSettingMixin, types.BooleanPreference): verbose_name = 'Federation enabled' help_text = ( 'Use this setting to enable or disable federation logic and API' - ' globally' + ' globally.' ) @@ -41,8 +44,11 @@ class CollectionPageSize( setting = 'FEDERATION_COLLECTION_PAGE_SIZE' verbose_name = 'Federation collection page size' help_text = ( - 'How much items to display in ActivityPub collections' + 'How much items to display in ActivityPub collections.' ) + field_kwargs = { + 'required': False, + } @global_preferences_registry.register @@ -54,8 +60,11 @@ class ActorFetchDelay( verbose_name = 'Federation actor fetch delay' help_text = ( 'How much minutes to wait before refetching actors on ' - 'request authentication' + 'request authentication.' ) + field_kwargs = { + 'required': False, + } @global_preferences_registry.register @@ -66,6 +75,6 @@ class MusicNeedsApproval( setting = 'FEDERATION_MUSIC_NEEDS_APPROVAL' verbose_name = 'Federation music needs approval' help_text = ( - 'When true, other federation actors will require your approval' + 'When true, other federation actors will need your approval' ' before being able to browse your library.' ) diff --git a/api/funkwhale_api/instance/dynamic_preferences_registry.py b/api/funkwhale_api/instance/dynamic_preferences_registry.py index 20679fd3dcff4cec208477487b6d245219abc273..8ccf80dd93448b4f2512ec2109cda32beaa33bf7 100644 --- a/api/funkwhale_api/instance/dynamic_preferences_registry.py +++ b/api/funkwhale_api/instance/dynamic_preferences_registry.py @@ -13,8 +13,11 @@ class InstanceName(types.StringPreference): section = instance name = 'name' default = '' - help_text = 'Instance public name' - verbose_name = 'The public name of your instance' + verbose_name = 'Public name' + help_text = 'The public name of your instance, displayed in the about page.' + field_kwargs = { + 'required': False, + } @global_preferences_registry.register @@ -23,7 +26,11 @@ class InstanceShortDescription(types.StringPreference): section = instance name = 'short_description' default = '' - verbose_name = 'Instance succinct description' + verbose_name = 'Short description' + help_text = 'Instance succinct description, displayed in the about page.' + field_kwargs = { + 'required': False, + } @global_preferences_registry.register @@ -31,31 +38,31 @@ class InstanceLongDescription(types.StringPreference): show_in_api = True section = instance name = 'long_description' + verbose_name = 'Long description' default = '' - help_text = 'Instance long description (markdown allowed)' + help_text = 'Instance long description, displayed in the about page (markdown allowed).' + widget = widgets.Textarea field_kwargs = { - 'widget': widgets.Textarea + 'required': False, } + @global_preferences_registry.register class RavenDSN(types.StringPreference): show_in_api = True section = raven name = 'front_dsn' default = 'https://9e0562d46b09442bb8f6844e50cbca2b@sentry.eliotberriot.com/4' - verbose_name = ( - 'A raven DSN key used to report front-ent errors to ' - 'a sentry instance' - ) + verbose_name = 'Raven DSN key (front-end)' + help_text = ( - 'Keeping the default one will report errors to funkwhale developers' + 'A Raven DSN key used to report front-ent errors to ' + 'a sentry instance. Keeping the default one will report errors to ' + 'Funkwhale developers.' ) - - -SENTRY_HELP_TEXT = ( - 'Error reporting is disabled by default but you can enable it if' - ' you want to help us improve funkwhale' -) + field_kwargs = { + 'required': False, + } @global_preferences_registry.register @@ -65,8 +72,7 @@ class RavenEnabled(types.BooleanPreference): name = 'front_enabled' default = False verbose_name = ( - 'Wether error reporting to a Sentry instance using raven is enabled' - ' for front-end errors' + 'Report front-end errors with Raven' ) @@ -78,7 +84,7 @@ class InstanceNodeinfoEnabled(types.BooleanPreference): default = True verbose_name = 'Enable nodeinfo endpoint' help_text = ( - 'This endpoint is needed for your about page to work.' + 'This endpoint is needed for your about page to work. ' 'It\'s also helpful for the various monitoring ' 'tools that map and analyzize the fediverse, ' 'but you can disable it completely if needed.' @@ -91,10 +97,10 @@ class InstanceNodeinfoPrivate(types.BooleanPreference): section = instance name = 'nodeinfo_private' default = False - verbose_name = 'Enable nodeinfo endpoint' + verbose_name = 'Private mode in nodeinfo' help_text = ( - 'Indicate in the nodeinfo endpoint that you do not want your instance' - 'to be tracked by third-party services.' + 'Indicate in the nodeinfo endpoint that you do not want your instance ' + 'to be tracked by third-party services. ' 'There is no guarantee these tools will honor this setting though.' ) @@ -107,6 +113,6 @@ class InstanceNodeinfoStatsEnabled(types.BooleanPreference): default = True verbose_name = 'Enable usage and library stats in nodeinfo endpoint' help_text = ( - 'Disable this f you don\'t want to share usage and library statistics' + 'Disable this if you don\'t want to share usage and library statistics ' 'in the nodeinfo endpoint but don\'t want to disable it completely.' ) diff --git a/api/funkwhale_api/instance/urls.py b/api/funkwhale_api/instance/urls.py index f506488fc4db7819da6aae5d5c79fe33e8a9af5c..7992842c030c636057ed13236da282febda9cc4e 100644 --- a/api/funkwhale_api/instance/urls.py +++ b/api/funkwhale_api/instance/urls.py @@ -1,9 +1,11 @@ from django.conf.urls import url +from rest_framework import routers from . import views - +admin_router = routers.SimpleRouter() +admin_router.register(r'admin/settings', views.AdminSettings, 'admin-settings') urlpatterns = [ url(r'^nodeinfo/2.0/$', views.NodeInfo.as_view(), name='nodeinfo-2.0'), url(r'^settings/$', views.InstanceSettings.as_view(), name='settings'), -] +] + admin_router.urls diff --git a/api/funkwhale_api/instance/views.py b/api/funkwhale_api/instance/views.py index 5953ca555a3081d5e58a1d60da1d3dec58279e3b..e6725e24846500f86bfbf9105d55b2a6780dc5dd 100644 --- a/api/funkwhale_api/instance/views.py +++ b/api/funkwhale_api/instance/views.py @@ -2,6 +2,7 @@ from rest_framework import views from rest_framework.response import Response from dynamic_preferences.api import serializers +from dynamic_preferences.api import viewsets as preferences_viewsets from dynamic_preferences.registries import global_preferences_registry from funkwhale_api.common import preferences @@ -15,6 +16,10 @@ NODEINFO_2_CONTENT_TYPE = ( ) +class AdminSettings(preferences_viewsets.GlobalPreferencesViewSet): + pagination_class = None + + class InstanceSettings(views.APIView): permission_classes = [] authentication_classes = [] diff --git a/api/funkwhale_api/playlists/dynamic_preferences_registry.py b/api/funkwhale_api/playlists/dynamic_preferences_registry.py index 21140fa1495adb734aa64e41be4bae218ba74304..b717177a2368ccfb267f651bd76def8c6e9fc443 100644 --- a/api/funkwhale_api/playlists/dynamic_preferences_registry.py +++ b/api/funkwhale_api/playlists/dynamic_preferences_registry.py @@ -13,3 +13,6 @@ class MaxTracks(preferences.DefaultFromSettingMixin, types.IntegerPreference): name = 'max_tracks' verbose_name = 'Max tracks per playlist' setting = 'PLAYLISTS_MAX_TRACKS' + field_kwargs = { + 'required': False, + } diff --git a/api/funkwhale_api/providers/acoustid/dynamic_preferences_registry.py b/api/funkwhale_api/providers/acoustid/dynamic_preferences_registry.py index da785df4065aff76cf8d1a4e9d19967b0b7bfa9c..33c9643b00b4ec96665a7e8cbcfae13098d13a04 100644 --- a/api/funkwhale_api/providers/acoustid/dynamic_preferences_registry.py +++ b/api/funkwhale_api/providers/acoustid/dynamic_preferences_registry.py @@ -1,3 +1,5 @@ +from django import forms + from dynamic_preferences.types import StringPreference, Section from dynamic_preferences.registries import global_preferences_registry @@ -11,3 +13,7 @@ class APIKey(StringPreference): default = '' verbose_name = 'Acoustid API key' help_text = 'The API key used to query AcoustID. Get one at https://acoustid.org/new-application.' + widget = forms.PasswordInput + field_kwargs = { + 'required': False, + } diff --git a/api/funkwhale_api/providers/youtube/dynamic_preferences_registry.py b/api/funkwhale_api/providers/youtube/dynamic_preferences_registry.py index fc7f7d793e5a535f344d63678f418b0736ae2862..ac5fc4bde29a313429b7644b51846fa3e33b0465 100644 --- a/api/funkwhale_api/providers/youtube/dynamic_preferences_registry.py +++ b/api/funkwhale_api/providers/youtube/dynamic_preferences_registry.py @@ -1,3 +1,5 @@ +from django import forms + from dynamic_preferences.types import StringPreference, Section from dynamic_preferences.registries import global_preferences_registry @@ -11,3 +13,7 @@ class APIKey(StringPreference): default = 'CHANGEME' verbose_name = 'YouTube API key' help_text = 'The API key used to query YouTube. Get one at https://console.developers.google.com/.' + widget = forms.PasswordInput + field_kwargs = { + 'required': False, + } diff --git a/api/funkwhale_api/users/dynamic_preferences_registry.py b/api/funkwhale_api/users/dynamic_preferences_registry.py index 16d79da143cb3139f9a92137044b6092288e1b40..4f736053088df4fad3ccd804744c8dec826d388f 100644 --- a/api/funkwhale_api/users/dynamic_preferences_registry.py +++ b/api/funkwhale_api/users/dynamic_preferences_registry.py @@ -10,6 +10,7 @@ class RegistrationEnabled(types.BooleanPreference): section = users name = 'registration_enabled' default = False - verbose_name = ( - 'Can visitors open a new account on this instance?' + verbose_name = 'Open registrations to new users' + help_text = ( + 'When enabled, new users will be able to register on this instance.' ) diff --git a/api/funkwhale_api/users/models.py b/api/funkwhale_api/users/models.py index f067a2a8b44b4bfbd61b8f7af86829301d178da0..8273507c49bb23f1986b6b691fe90cc1fc8fea45 100644 --- a/api/funkwhale_api/users/models.py +++ b/api/funkwhale_api/users/models.py @@ -6,7 +6,7 @@ import os import uuid from django.conf import settings -from django.contrib.auth.models import AbstractUser +from django.contrib.auth.models import AbstractUser, Permission from django.urls import reverse from django.db import models from django.utils.encoding import python_2_unicode_compatible @@ -55,6 +55,10 @@ class User(AbstractUser): def __str__(self): return self.username + def add_permission(self, codename): + p = Permission.objects.get(codename=codename) + self.user_permissions.add(p) + def get_absolute_url(self): return reverse('users:detail', kwargs={'username': self.username}) diff --git a/api/tests/instance/test_views.py b/api/tests/instance/test_views.py index 468c0ddae9de440b3edce7fd65fdc57c6ead8fff..6d8dcac3eebe1470da291a783bb7ad97a8b0c127 100644 --- a/api/tests/instance/test_views.py +++ b/api/tests/instance/test_views.py @@ -21,3 +21,31 @@ def test_nodeinfo_endpoint_disabled(db, api_client, preferences): response = api_client.get(url) assert response.status_code == 404 + + +def test_settings_only_list_public_settings(db, api_client, preferences): + url = reverse('api:v1:instance:settings') + response = api_client.get(url) + + for conf in response.data: + p = preferences.model.objects.get( + section=conf['section'], name=conf['name']) + assert p.preference.show_in_api is True + + +def test_admin_settings_restrict_access(db, logged_in_api_client, preferences): + url = reverse('api:v1:instance:admin-settings-list') + response = logged_in_api_client.get(url) + + assert response.status_code == 403 + + +def test_admin_settings_correct_permission( + db, logged_in_api_client, preferences): + user = logged_in_api_client.user + user.add_permission('change_globalpreferencemodel') + url = reverse('api:v1:instance:admin-settings-list') + response = logged_in_api_client.get(url) + + assert response.status_code == 200 + assert len(response.data) == len(preferences.all()) diff --git a/changes/changelog.d/206.feature b/changes/changelog.d/206.feature new file mode 100644 index 0000000000000000000000000000000000000000..b334554fe0a3243ae7f85f3386ed714f6ed8a870 --- /dev/null +++ b/changes/changelog.d/206.feature @@ -0,0 +1,17 @@ +We now have a brand new instance settings interface in the front-end (#206) + + +Instance settings interface +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Prior to this release, the only way to update instance settings (such as +instance description, signup policy, federation configuration, etc.) was using +the admin interface provided by Django (the back-end framework which power the API). + +This interface worked, but was not really-user friendly and intuitive. + +Starting from this release, we now offer a dedicated interface directly +in the front-end. You can view and edit all your instance settings from here, +assuming you have the required permissions. + +This interface is available at ``/manage/settings` and via link in the sidebar. diff --git a/docs/configuration.rst b/docs/configuration.rst index bbc658e087977e5eaa7c3f53598fdad99d68b4aa..b7df2db42079304fe9956a5460ea6f9beb4e746d 100644 --- a/docs/configuration.rst +++ b/docs/configuration.rst @@ -27,15 +27,24 @@ Those settings are stored in database and do not require a restart of your instance after modification. They typically relate to higher level configuration, such your instance description, signup policy and so on. -There is no polished interface for those settings, yet, but you can view update -them using the administration interface provided by Django (the framework funkwhale is built on). - -The URL should be ``/api/admin/dynamic_preferences/globalpreferencemodel/`` (prepend your domain in front of it, of course). +You can edit those settings directly from the web application, assuming +you have the required permissions. The URL is ``/manage/settings``, and +you will also find a link to this page in the sidebar. If you plan to use acoustid and external imports (e.g. with the youtube backends), you should edit the corresponding settings in this interface. +.. note:: + + If you have any issue with the web application, a management interface is also + available for those settings from Django's administration interface. It's + less user friendly, though, and we recommend you use the web app interface + whenever possible. + + The URL should be ``/api/admin/dynamic_preferences/globalpreferencemodel/`` (prepend your domain in front of it, of course). + + Configuration reference ----------------------- diff --git a/front/src/components/About.vue b/front/src/components/About.vue index 524191250cf920fdbe4991381f827c1e50dd5ecd..b0ae67ef7ae554aa70a2ed6cc595b45fea81b32d 100644 --- a/front/src/components/About.vue +++ b/front/src/components/About.vue @@ -13,6 +13,12 @@ <p v-if="!instance.short_description.value && !instance.long_description.value"> {{ $t('Unfortunately, owners of this instance did not yet take the time to complete this page.') }} </p> + <router-link + class="ui button" + v-if="$store.state.auth.availablePermissions['settings.change']" + :to="{path: '/manage/settings', hash: 'instance'}"> + <i class="pencil icon"></i>{{ $t('Edit instance info') }} + </router-link> <div v-if="instance.short_description.value" class="ui middle aligned stackable text container"> diff --git a/front/src/components/Sidebar.vue b/front/src/components/Sidebar.vue index 97c743bbe804a5ca64bfe39ce69ad5b725745365..9fbc5605c8e13923379e879b4b1a6b3322d14333 100644 --- a/front/src/components/Sidebar.vue +++ b/front/src/components/Sidebar.vue @@ -78,6 +78,12 @@ :title="$t('Pending follow requests')"> {{ notifications.federation }}</div> </router-link> + <router-link + class="item" + v-if="$store.state.auth.availablePermissions['settings.change']" + :to="{path: '/manage/settings'}"> + <i class="settings icon"></i>{{ $t('Settings') }} + </router-link> </div> </div> </div> @@ -217,7 +223,6 @@ export default { } let self = this axios.get('requests/import-requests/', {params: {status: 'pending'}}).then(response => { - console.log('YOLo') self.notifications.importRequests = response.data.count }) }, @@ -256,7 +261,6 @@ export default { }, '$store.state.availablePermissions': { handler () { - console.log('YOLO') this.fetchNotificationsCount() }, deep: true diff --git a/front/src/components/admin/SettingsGroup.vue b/front/src/components/admin/SettingsGroup.vue new file mode 100644 index 0000000000000000000000000000000000000000..255f04488973fbc4eb5c1ecc5e97418294c258ff --- /dev/null +++ b/front/src/components/admin/SettingsGroup.vue @@ -0,0 +1,120 @@ +<template> + <form :id="group.id" class="ui form" @submit.prevent="save"> + <div class="ui divider" /> + <h3 class="ui header">{{ group.label }}</h3> + <div v-if="errors.length > 0" class="ui negative message"> + <div class="header">{{ $t('Error while saving settings') }}</div> + <ul class="list"> + <li v-for="error in errors">{{ error }}</li> + </ul> + </div> + <div v-if="result" class="ui positive message"> + {{ $t('Settings updated successfully.') }} + </div> + <p v-if="group.help">{{ group.help }}</p> + <div v-for="setting in settings" class="ui field"> + <template v-if="setting.field.widget.class !== 'CheckboxInput'"> + <label :for="setting.identifier">{{ setting.verbose_name }}</label> + <p v-if="setting.help_text">{{ setting.help_text }}</p> + </template> + <input + :id="setting.identifier" + v-if="setting.field.widget.class === 'PasswordInput'" + type="password" + class="ui input" + v-model="values[setting.identifier]" /> + <input + :id="setting.identifier" + v-if="setting.field.widget.class === 'TextInput'" + type="text" + class="ui input" + v-model="values[setting.identifier]" /> + <input + :id="setting.identifier" + v-if="setting.field.class === 'IntegerField'" + type="number" + class="ui input" + v-model.number="values[setting.identifier]" /> + <textarea + :id="setting.identifier" + v-else-if="setting.field.widget.class === 'Textarea'" + type="text" + class="ui input" + v-model="values[setting.identifier]" /> + <div v-else-if="setting.field.widget.class === 'CheckboxInput'" class="ui toggle checkbox"> + <input + :id="setting.identifier" + :name="setting.identifier" + v-model="values[setting.identifier]" + type="checkbox" /> + <label :for="setting.identifier">{{ setting.verbose_name }}</label> + <p v-if="setting.help_text">{{ setting.help_text }}</p> + </div> + </div> + <button + type="submit" + :class="['ui', {'loading': isLoading}, 'right', 'floated', 'green', 'button']"> + {{ $t('Save') }} + </button> + </form> +</template> + +<script> +import axios from 'axios' + +export default { + props: { + group: {type: Object, required: true}, + settingsData: {type: Array, required: true} + }, + data () { + return { + values: {}, + result: null, + errors: [], + isLoading: false + } + }, + created () { + let self = this + this.settings.forEach(e => { + self.values[e.identifier] = e.value + }) + }, + methods: { + save () { + let self = this + this.isLoading = true + self.errors = [] + self.result = null + axios.post('instance/admin/settings/bulk/', self.values).then((response) => { + self.result = true + self.isLoading = false + self.$store.dispatch('instance/fetchSettings') + }, error => { + self.isLoading = false + self.errors = error.backendErrors + }) + } + }, + computed: { + settings () { + let byIdentifier = {} + this.settingsData.forEach(e => { + byIdentifier[e.identifier] = e + }) + return this.group.settings.map(e => { + return byIdentifier[e] + }) + } + } +} +</script> + +<!-- Add "scoped" attribute to limit CSS to this component only --> +<style scoped> + +.ui.checkbox p { + margin-top: 1rem; +} +</style> diff --git a/front/src/main.js b/front/src/main.js index 5481615f2006025cea7e009dc9ddd8a49b97623f..2e92fbbd2243e20f567f47dcbcc5e5f3c7937659 100644 --- a/front/src/main.js +++ b/front/src/main.js @@ -35,8 +35,26 @@ Vue.use(VueMasonryPlugin) Vue.use(VueLazyload) Vue.config.productionTip = false Vue.directive('title', { - inserted: (el, binding) => { document.title = binding.value + ' - Funkwhale' }, - updated: (el, binding) => { document.title = binding.value + ' - Funkwhale' } + inserted: (el, binding) => { + let parts = [] + let instanceName = store.state.instance.settings.instance.name.value + if (instanceName.length === 0) { + instanceName = 'Funkwhale' + } + parts.unshift(instanceName) + parts.unshift(binding.value) + document.title = parts.join(' - ') + }, + updated: (el, binding) => { + let parts = [] + let instanceName = store.state.instance.settings.instance.name.value + if (instanceName.length === 0) { + instanceName = 'Funkwhale' + } + parts.unshift(instanceName) + parts.unshift(binding.value) + document.title = parts.join(' - ') + } }) axios.defaults.baseURL = config.API_URL diff --git a/front/src/router/index.js b/front/src/router/index.js index b1e208023335945f42c7b255aa05ebaffeac5e2b..f71dab7f92decf05f5df0ca2129207bf7830fb48 100644 --- a/front/src/router/index.js +++ b/front/src/router/index.js @@ -28,6 +28,7 @@ import RequestsList from '@/components/requests/RequestsList' import PlaylistDetail from '@/views/playlists/Detail' import PlaylistList from '@/views/playlists/List' import Favorites from '@/components/favorites/List' +import AdminSettings from '@/views/admin/Settings' import FederationBase from '@/views/federation/Base' import FederationScan from '@/views/federation/Scan' import FederationLibraryDetail from '@/views/federation/LibraryDetail' @@ -117,6 +118,11 @@ export default new Router({ defaultPaginateBy: route.query.paginateBy }) }, + { + path: '/manage/settings', + name: 'manage.settings', + component: AdminSettings + }, { path: '/manage/federation', component: FederationBase, diff --git a/front/src/views/admin/Settings.vue b/front/src/views/admin/Settings.vue new file mode 100644 index 0000000000000000000000000000000000000000..7174ab516c1e438d0aa09b0491e803b26f5f4316 --- /dev/null +++ b/front/src/views/admin/Settings.vue @@ -0,0 +1,155 @@ +<template> + <div class="main pusher" v-title="$t('Instance settings')"> + <div class="ui vertical stripe segment"> + <div class="ui text container"> + <div :class="['ui', {'loading': isLoading}, 'form']"></div> + <div id="settings-grid" v-if="settingsData" class="ui grid"> + <div class="twelve wide stretched column"> + <settings-group + :settings-data="settingsData" + :group="group" + :key="group.title" + v-for="group in groups" /> + </div> + <div class="four wide column"> + <div class="ui sticky vertical secondary menu"> + <div class="header item">{{ $t('Sections') }}</div> + <a :class="['menu', {active: group.id === current}, 'item']" + @click.prevent="scrollTo(group.id)" + :href="'#' + group.id" + v-for="group in groups">{{ group.label }}</a> + </div> + </div> + </div> + + </div> + </div> + </div> +</template> + +<script> +import axios from 'axios' +import $ from 'jquery' + +import SettingsGroup from '@/components/admin/SettingsGroup' + +export default { + components: { + SettingsGroup + }, + data () { + return { + isLoading: false, + settingsData: null, + current: null + } + }, + created () { + let self = this + this.fetchSettings().then(r => { + self.$nextTick(() => { + if (self.$store.state.route.hash) { + self.scrollTo(self.$store.state.route.hash.substr(1)) + } + }) + }) + }, + methods: { + scrollTo (id) { + console.log(id, 'hello') + this.current = id + document.getElementById(id).scrollIntoView() + }, + fetchSettings () { + let self = this + self.isLoading = true + return axios.get('instance/admin/settings/').then((response) => { + self.settingsData = response.data + self.isLoading = false + }) + } + }, + computed: { + groups () { + return [ + { + label: this.$t('Instance information'), + id: 'instance', + settings: [ + 'instance__name', + 'instance__short_description', + 'instance__long_description' + ] + }, + { + label: this.$t('Users'), + id: 'users', + settings: [ + 'users__registration_enabled', + 'common__api_authentication_required' + ] + }, + { + label: this.$t('Imports'), + id: 'imports', + settings: [ + 'providers_youtube__api_key', + 'providers_acoustid__api_key' + ] + }, + { + label: this.$t('Playlists'), + id: 'playlists', + settings: [ + 'playlists__max_tracks' + ] + }, + { + label: this.$t('Federation'), + id: 'federation', + settings: [ + 'federation__enabled', + 'federation__music_needs_approval', + 'federation__collection_page_size', + 'federation__music_cache_duration', + 'federation__actor_fetch_delay' + ] + }, + { + label: this.$t('Subsonic'), + id: 'subsonic', + settings: [ + 'subsonic__enabled' + ] + }, + { + label: this.$t('Statistics'), + id: 'statistics', + settings: [ + 'instance__nodeinfo_enabled', + 'instance__nodeinfo_stats_enabled', + 'instance__nodeinfo_private' + ] + }, + { + label: this.$t('Error reporting'), + id: 'reporting', + settings: [ + 'raven__front_enabled', + 'raven__front_dsn' + + ] + } + ] + } + }, + watch: { + settingsData () { + let self = this + this.$nextTick(() => { + $(self.$el).find('.sticky').sticky({context: '#settings-grid'}) + }) + } + } +} +</script> diff --git a/front/src/views/playlists/List.vue b/front/src/views/playlists/List.vue index 32ee5aafaa9d28e34d1c824f479c6b0efea0447f..5001fb14db9db32bd129e0fdcefa3a04f60a848b 100644 --- a/front/src/views/playlists/List.vue +++ b/front/src/views/playlists/List.vue @@ -76,7 +76,6 @@ export default { Pagination }, data () { - console.log('YOLO', this.$t) let defaultOrdering = this.getOrderingFromString(this.defaultOrdering || '-creation_date') return { isLoading: true,