diff --git a/api/config/urls.py b/api/config/urls.py
index 8c490a5e6599e2f44bb60bb110a6a362f4153f93..de67ebb571de4b5f4e15cedd969d1adadeb42aee 100644
--- a/api/config/urls.py
+++ b/api/config/urls.py
@@ -31,3 +31,9 @@ if settings.DEBUG:
url(r'^404/$', default_views.page_not_found),
url(r'^500/$', default_views.server_error),
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
+
+ if 'debug_toolbar' in settings.INSTALLED_APPS:
+ import debug_toolbar
+ urlpatterns += [
+ url(r'^__debug__/', include(debug_toolbar.urls)),
+ ]
diff --git a/api/funkwhale_api/music/filters.py b/api/funkwhale_api/music/filters.py
index ba3fa453d77443eb75247b3987849db972963e1c..ff937a0f5c3aac8d5609b188d18b796e3b809e3a 100644
--- a/api/funkwhale_api/music/filters.py
+++ b/api/funkwhale_api/music/filters.py
@@ -8,5 +8,5 @@ class ArtistFilter(django_filters.FilterSet):
class Meta:
model = models.Artist
fields = {
- 'name': ['exact', 'iexact', 'startswith']
+ 'name': ['exact', 'iexact', 'startswith', 'icontains']
}
diff --git a/api/funkwhale_api/music/serializers.py b/api/funkwhale_api/music/serializers.py
index 43daf9d5a19404309b3ccf1f85e2e4c4713a9840..cf9d8749021ca8f5a1c03f297924de2ed1a583ca 100644
--- a/api/funkwhale_api/music/serializers.py
+++ b/api/funkwhale_api/music/serializers.py
@@ -13,14 +13,14 @@ class TagSerializer(serializers.ModelSerializer):
class SimpleArtistSerializer(serializers.ModelSerializer):
class Meta:
model = models.Artist
- fields = ('id', 'mbid', 'name')
+ fields = ('id', 'mbid', 'name', 'creation_date')
class ArtistSerializer(serializers.ModelSerializer):
tags = TagSerializer(many=True, read_only=True)
class Meta:
model = models.Artist
- fields = ('id', 'mbid', 'name', 'tags')
+ fields = ('id', 'mbid', 'name', 'tags', 'creation_date')
class TrackFileSerializer(serializers.ModelSerializer):
diff --git a/api/funkwhale_api/music/views.py b/api/funkwhale_api/music/views.py
index c32fa8f7ff49caa1ef9fbfff533e12a32835373e..5bfefc29b8c79f7521984f866f258930f84aa821 100644
--- a/api/funkwhale_api/music/views.py
+++ b/api/funkwhale_api/music/views.py
@@ -47,16 +47,15 @@ class TagViewSetMixin(object):
class ArtistViewSet(SearchMixin, viewsets.ReadOnlyModelViewSet):
queryset = (
models.Artist.objects.all()
- .order_by('name')
.prefetch_related(
'albums__tracks__files',
+ 'albums__tracks__artist',
'albums__tracks__tags'))
serializer_class = serializers.ArtistSerializerNested
permission_classes = [ConditionalAuthentication]
search_fields = ['name']
- ordering_fields = ('creation_date', 'name')
filter_class = filters.ArtistFilter
-
+ ordering_fields = ('id', 'name', 'creation_date')
class AlbumViewSet(SearchMixin, viewsets.ReadOnlyModelViewSet):
queryset = (
diff --git a/front/src/App.vue b/front/src/App.vue
index f81d7d3daae22224f9d32450844a9677302c111e..c1c0874998b21af43407da9c87bb5ecb37ecd02b 100644
--- a/front/src/App.vue
+++ b/front/src/App.vue
@@ -1,7 +1,7 @@
<template>
<div id="app">
<sidebar></sidebar>
- <router-view></router-view>
+ <router-view :key="$route.fullPath"></router-view>
<div class="ui divider"></div>
<div id="footer" class="ui vertical footer segment">
<div class="ui container">
diff --git a/front/src/components/library/Artists.vue b/front/src/components/library/Artists.vue
index 2f0fb0a9236197d725191d4d1106037f855d3955..c3e9f1d14534e8486b5d72f156a83b7cbb7cb214 100644
--- a/front/src/components/library/Artists.vue
+++ b/front/src/components/library/Artists.vue
@@ -1,11 +1,40 @@
<template>
<div>
- <div v-if="isLoading" class="ui vertical segment">
- <div :class="['ui', 'centered', 'active', 'inline', 'loader']"></div>
- </div>
- <div v-if="result" class="ui vertical stripe segment">
+ <div class="ui vertical stripe segment">
<h2 class="ui header">Browsing artists</h2>
- <div class="ui stackable three column grid">
+ <div :class="['ui', {'loading': isLoading}, 'form']">
+ <div class="fields">
+ <div class="field">
+ <label>Search</label>
+ <input type="text" v-model="query" placeholder="Enter an artist name..."/>
+ </div>
+ <div class="field">
+ <label>Ordering</label>
+ <select class="ui dropdown" v-model="ordering">
+ <option v-for="option in orderingOptions" :value="option[0]">
+ {{ option[1] }}
+ </option>
+ </select>
+ </div>
+ <div class="field">
+ <label>Ordering direction</label>
+ <select class="ui dropdown" v-model="orderingDirection">
+ <option value="">Ascending</option>
+ <option value="-">Descending</option>
+ </select>
+ </div>
+ <div class="field">
+ <label>Results per page</label>
+ <select class="ui dropdown" v-model="paginateBy">
+ <option :value="parseInt(12)">12</option>
+ <option :value="parseInt(25)">25</option>
+ <option :value="parseInt(50)">50</option>
+ </select>
+ </div>
+ </div>
+ </div>
+ <div class="ui hidden divider"></div>
+ <div v-if="result" class="ui stackable three column grid">
<div
v-if="result.results.length > 0"
v-for="artist in result.results"
@@ -28,6 +57,8 @@
</template>
<script>
+import _ from 'lodash'
+import $ from 'jquery'
import config from '@/config'
import backend from '@/audio/backend'
@@ -38,31 +69,72 @@ import Pagination from '@/components/Pagination'
const FETCH_URL = config.API_URL + 'artists/'
export default {
+ props: {
+ defaultOrdering: {type: String, required: false, default: '-creation_date'},
+ defaultQuery: {type: String, required: false, default: ''},
+ defaultPage: {required: false, default: 1},
+ defaultPaginateBy: {required: false, default: 12}
+ },
components: {
ArtistCard,
Pagination
},
data () {
+ let defaultOrdering = this.getOrderingFromString(this.defaultOrdering)
return {
isLoading: true,
result: null,
- page: 1,
- orderBy: 'name',
- paginateBy: 12
+ page: parseInt(this.defaultPage),
+ query: this.defaultQuery,
+ paginateBy: parseInt(this.defaultPaginateBy),
+ orderingDirection: defaultOrdering.direction,
+ ordering: defaultOrdering.field,
+ orderingOptions: [
+ ['creation_date', 'Creation date'],
+ ['name', 'Name']
+ ]
}
},
created () {
this.fetchData()
},
+ mounted () {
+ $('.ui.dropdown').dropdown()
+ },
methods: {
- fetchData () {
+ getOrderingFromString (s) {
+ let parts = s.split('-')
+ if (parts.length > 1) {
+ return {
+ direction: '-',
+ field: parts.slice(1).join('-')
+ }
+ } else {
+ return {
+ direction: '',
+ field: s
+ }
+ }
+ },
+ updateQueryString: function () {
+ this.$router.replace({
+ query: {
+ query: this.query,
+ page: this.page,
+ paginateBy: this.paginateBy,
+ ordering: [this.orderingDirection, this.ordering].join('')
+ }
+ })
+ },
+ fetchData: _.debounce(function () {
var self = this
this.isLoading = true
let url = FETCH_URL
let params = {
page: this.page,
page_size: this.paginateBy,
- order_by: 'name'
+ name__icontains: this.query,
+ ordering: [this.orderingDirection, this.ordering].join('')
}
logger.default.debug('Fetching artists')
this.$http.get(url, {params: params}).then((response) => {
@@ -76,13 +148,30 @@ export default {
})
self.isLoading = false
})
- },
+ }, 500),
selectPage: function (page) {
this.page = page
}
},
watch: {
page () {
+ this.updateQueryString()
+ this.fetchData()
+ },
+ paginateBy () {
+ this.updateQueryString()
+ this.fetchData()
+ },
+ ordering () {
+ this.updateQueryString()
+ this.fetchData()
+ },
+ orderingDirection () {
+ this.updateQueryString()
+ this.fetchData()
+ },
+ query () {
+ this.updateQueryString()
this.fetchData()
}
}
diff --git a/front/src/components/library/Library.vue b/front/src/components/library/Library.vue
index da9ac19b3ad7cef127384b446296d76e9a39a638..e8b053b6d0124175fda02254e71a85a420401e5e 100644
--- a/front/src/components/library/Library.vue
+++ b/front/src/components/library/Library.vue
@@ -8,7 +8,7 @@
<router-link v-if="auth.user.availablePermissions['import.launch']" class="ui item" to="/library/import/batches">Import batches</router-link>
</div>
</div>
- <router-view></router-view>
+ <router-view :key="$route.fullPath"></router-view>
</div>
</template>
diff --git a/front/src/router/index.js b/front/src/router/index.js
index d727276fc7b58f58d288bf4ae42282341cc65ce4..f6653e73d5976514384696fab41acd52b16ec7e3 100644
--- a/front/src/router/index.js
+++ b/front/src/router/index.js
@@ -54,7 +54,17 @@ export default new Router({
component: Library,
children: [
{ path: '', component: LibraryHome },
- { path: 'artists/', name: 'library.artists.browse', component: LibraryArtists },
+ {
+ path: 'artists/',
+ name: 'library.artists.browse',
+ component: LibraryArtists,
+ props: (route) => ({
+ defaultOrdering: route.query.ordering,
+ defaultQuery: route.query.query,
+ defaultPaginateBy: route.query.paginateBy,
+ defaultPage: route.query.page
+ })
+ },
{ path: 'artists/:id', name: 'library.artists.detail', component: LibraryArtist, props: true },
{ path: 'albums/:id', name: 'library.albums.detail', component: LibraryAlbum, props: true },
{ path: 'tracks/:id', name: 'library.tracks.detail', component: LibraryTrack, props: true },