From 64cecf17a87e09a3a921b93384dac2cffceedf23 Mon Sep 17 00:00:00 2001 From: Eliot Berriot <contact@eliotberriot.com> Date: Thu, 21 Jun 2018 23:31:29 +0200 Subject: [PATCH] See #190: front-end to manage import requests --- front/src/components/library/Library.vue | 7 - .../manage/library/RequestsTable.vue | 229 ++++++++++++++++++ .../src/components/requests/RequestsList.vue | 198 --------------- front/src/router/index.js | 23 +- front/src/views/admin/library/Base.vue | 9 + .../src/views/admin/library/RequestsList.vue | 23 ++ 6 files changed, 268 insertions(+), 221 deletions(-) create mode 100644 front/src/components/manage/library/RequestsTable.vue delete mode 100644 front/src/components/requests/RequestsList.vue create mode 100644 front/src/views/admin/library/RequestsList.vue diff --git a/front/src/components/library/Library.vue b/front/src/components/library/Library.vue index 50337b22..5360de16 100644 --- a/front/src/components/library/Library.vue +++ b/front/src/components/library/Library.vue @@ -6,13 +6,6 @@ <router-link class="ui item" to="/library/radios" exact><i18next path="Radios"/></router-link> <router-link class="ui item" to="/library/playlists" exact><i18next path="Playlists"/></router-link> <div class="ui secondary right menu"> - <router-link - v-if="$store.state.auth.authenticated" - class="ui item" - :to="{name: 'library.requests', query: {status: 'pending' }}" - exact> - <i18next path="Requests"/> - </router-link> <router-link v-if="showImports" class="ui item" to="/library/import/launch" exact> <i18next path="Import"/> </router-link> diff --git a/front/src/components/manage/library/RequestsTable.vue b/front/src/components/manage/library/RequestsTable.vue new file mode 100644 index 00000000..e51b911a --- /dev/null +++ b/front/src/components/manage/library/RequestsTable.vue @@ -0,0 +1,229 @@ +<template> + <div> + <div class="ui inline form"> + <div class="fields"> + <div class="ui field"> + <label>{{ $t('Search') }}</label> + <input type="text" v-model="search" placeholder="Search by artist, username, comment..." /> + </div> + <div class="field"> + <i18next tag="label" path="Ordering"/> + <select class="ui dropdown" v-model="ordering"> + <option v-for="option in orderingOptions" :value="option[0]"> + {{ option[1] }} + </option> + </select> + </div> + <div class="field"> + <i18next tag="label" path="Ordering direction"/> + <select class="ui dropdown" v-model="orderingDirection"> + <option value="+">Ascending</option> + <option value="-">Descending</option> + </select> + </div> + <div class="field"> + <label>{{ $t("Status") }}</label> + <select class="ui dropdown" v-model="status"> + <option :value="null">{{ $t('All') }}</option> + <option :value="'pending'">{{ $t('Pending') }}</option> + <option :value="'accepted'">{{ $t('Accepted') }}</option> + <option :value="'imported'">{{ $t('Imported') }}</option> + <option :value="'closed'">{{ $t('Closed') }}</option> + </select> + </div> + </div> + </div> + <div class="dimmable"> + <div v-if="isLoading" class="ui active inverted dimmer"> + <div class="ui loader"></div> + </div> + <action-table + v-if="result" + @action-launched="fetchData" + :objects-data="result" + :actions="actions" + :action-url="'manage/requests/import-requests/action/'" + :filters="actionFilters"> + <template slot="header-cells"> + <th>{{ $t('User') }}</th> + <th>{{ $t('Status') }}</th> + <th>{{ $t('Artist') }}</th> + <th>{{ $t('Albums') }}</th> + <th>{{ $t('Comment') }}</th> + <th>{{ $t('Creation date') }}</th> + <th>{{ $t('Import date') }}</th> + <th>{{ $t('Actions') }}</th> + </template> + <template slot="row-cells" slot-scope="scope"> + <td> + {{ scope.obj.user.username }} + </td> + <td> + <span class="ui green basic label" v-if="scope.obj.status === 'imported'">{{ $t('Imported') }}</span> + <span class="ui pink basic label" v-else-if="scope.obj.status === 'accepted'">{{ $t('Accepted') }}</span> + <span class="ui yellow basic label" v-else-if="scope.obj.status === 'pending'">{{ $t('Pending') }}</span> + <span class="ui red basic label" v-else-if="scope.obj.status === 'closed'">{{ $t('Closed') }}</span> + </td> + <td> + <span :title="scope.obj.artist_name">{{ scope.obj.artist_name|truncate(30) }}</span> + </td> + <td> + <span v-if="scope.obj.albums" :title="scope.obj.albums">{{ scope.obj.albums|truncate(30) }}</span> + <template v-else>{{Â $t('N/A') }}</template> + </td> + <td> + <span v-if="scope.obj.comment" :title="scope.obj.comment">{{ scope.obj.comment|truncate(30) }}</span> + <template v-else>{{Â $t('N/A') }}</template> + </td> + <td> + <human-date :date="scope.obj.creation_date"></human-date> + </td> + <td> + <human-date v-if="scope.obj.imported_date" :date="scope.obj.creation_date"></human-date> + <template v-else>{{Â $t('N/A') }}</template> + </td> + <td> + <router-link + class="ui tiny basic button" + :to="{name: 'library.import.launch', query: {request: scope.obj.id}}" + v-if="scope.obj.status === 'pending'">{{Â $t('Create import') }}</router-link> + </td> + </template> + </action-table> + </div> + <div> + <pagination + v-if="result && result.results.length > 0" + @page-changed="selectPage" + :compact="true" + :current="page" + :paginate-by="paginateBy" + :total="result.count" + ></pagination> + + <span v-if="result && result.results.length > 0"> + {{ $t('Showing results {%start%}-{%end%} on {%total%}', {start: ((page-1) * paginateBy) + 1 , end: ((page-1) * paginateBy) + result.results.length, total: result.count})}} + </span> + </div> + </div> +</template> + +<script> +import axios from 'axios' +import _ from 'lodash' +import time from '@/utils/time' +import Pagination from '@/components/Pagination' +import ActionTable from '@/components/common/ActionTable' +import OrderingMixin from '@/components/mixins/Ordering' + +export default { + mixins: [OrderingMixin], + props: { + filters: {type: Object, required: false} + }, + components: { + Pagination, + ActionTable + }, + data () { + let defaultOrdering = this.getOrderingFromString(this.defaultOrdering || '-creation_date') + return { + time, + isLoading: false, + result: null, + page: 1, + paginateBy: 25, + search: '', + status: null, + orderingDirection: defaultOrdering.direction || '+', + ordering: defaultOrdering.field, + orderingOptions: [ + ['creation_date', 'Creation date'], + ['imported_date', 'Imported date'] + ] + + } + }, + created () { + this.fetchData() + }, + methods: { + fetchData () { + let params = _.merge({ + 'page': this.page, + 'page_size': this.paginateBy, + 'q': this.search, + 'status': this.status, + 'ordering': this.getOrderingAsString() + }, this.filters) + let self = this + self.isLoading = true + self.checked = [] + axios.get('/manage/requests/import-requests/', {params: params}).then((response) => { + self.result = response.data + self.isLoading = false + }, error => { + self.isLoading = false + self.errors = error.backendErrors + }) + }, + selectPage: function (page) { + this.page = page + } + }, + computed: { + actionFilters () { + var currentFilters = { + q: this.search + } + if (this.filters) { + return _.merge(currentFilters, this.filters) + } else { + return currentFilters + } + }, + actions () { + return [ + { + name: 'delete', + label: this.$t('Delete'), + isDangerous: true + }, + { + name: 'mark_imported', + label: this.$t('Mark as imported'), + filterCheckable: (obj) => { return ['pending', 'accepted'].indexOf(obj.status) > -1 }, + isDangerous: true + }, + { + name: 'mark_closed', + label: this.$t('Mark as closed'), + filterCheckable: (obj) => { return ['pending', 'accepted'].indexOf(obj.status) > -1 }, + isDangerous: true + } + ] + } + }, + watch: { + search (newValue) { + this.page = 1 + this.fetchData() + }, + page () { + this.fetchData() + }, + ordering () { + this.page = 1 + this.fetchData() + }, + status () { + this.page = 1 + this.fetchData() + }, + orderingDirection () { + this.page = 1 + this.fetchData() + } + } +} +</script> diff --git a/front/src/components/requests/RequestsList.vue b/front/src/components/requests/RequestsList.vue deleted file mode 100644 index 58b7f5fa..00000000 --- a/front/src/components/requests/RequestsList.vue +++ /dev/null @@ -1,198 +0,0 @@ -<template> - <div v-title="'Import Requests'"> - <div class="ui vertical stripe segment"> - <h2 class="ui header">{{ $t('Music requests') }}</h2> - <div :class="['ui', {'loading': isLoading}, 'form']"> - <div class="fields"> - <div class="field"> - <label>{{ $t('Search') }}</label> - <input type="text" v-model="query" placeholder="Enter an artist name, a username..."/> - </div> - <div class="field"> - <label>{{ $t('Status') }}</label> - <select class="ui dropdown" v-model="status"> - <option :value="'any'">{{ $t('Any') }}</option> - <option :value="'pending'">{{ $t('Pending') }}</option> - <option :value="'accepted'">{{ $t('Accepted') }}</option> - <option :value="'imported'">{{ $t('Imported') }}</option> - <option :value="'closed'">{{ $t('Closed') }}</option> - </select> - </div> - <div class="field"> - <label>{{ $t('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>{{ $t('Ordering direction') }}</label> - <select class="ui dropdown" v-model="orderingDirection"> - <option value="+">Ascending</option> - <option value="-">Descending</option> - </select> - </div> - <div class="field"> - <label>{{ $t('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" - v-masonry - transition-duration="0" - item-selector=".column" - percent-position="true" - stagger="0" - class="ui stackable three column doubling grid"> - <div - v-masonry-tile - v-if="result.results.length > 0" - v-for="request in result.results" - :key="request.id" - class="column"> - <request-card class="fluid" :request="request"></request-card> - </div> - </div> - <div class="ui center aligned basic segment"> - <pagination - v-if="result && result.results.length > 0" - @page-changed="selectPage" - :current="page" - :paginate-by="paginateBy" - :total="result.count" - ></pagination> - </div> - </div> - </div> -</template> - -<script> -import axios from 'axios' -import _ from 'lodash' -import $ from 'jquery' - -import logger from '@/logging' - -import OrderingMixin from '@/components/mixins/Ordering' -import PaginationMixin from '@/components/mixins/Pagination' -import RequestCard from '@/components/requests/Card' -import Pagination from '@/components/Pagination' - -const FETCH_URL = 'requests/import-requests/' - -export default { - mixins: [OrderingMixin, PaginationMixin], - props: { - defaultQuery: {type: String, required: false, default: ''}, - defaultStatus: {required: false, default: 'any'} - }, - components: { - RequestCard, - Pagination - }, - data () { - let defaultOrdering = this.getOrderingFromString(this.defaultOrdering || '-creation_date') - return { - isLoading: true, - result: null, - page: parseInt(this.defaultPage), - query: this.defaultQuery, - paginateBy: parseInt(this.defaultPaginateBy || 12), - orderingDirection: defaultOrdering.direction || '+', - ordering: defaultOrdering.field, - status: this.defaultStatus || 'any' - } - }, - created () { - this.fetchData() - }, - mounted () { - $('.ui.dropdown').dropdown() - }, - methods: { - updateQueryString: _.debounce(function () { - let query = { - query: { - query: this.query, - page: this.page, - paginateBy: this.paginateBy, - ordering: this.getOrderingAsString() - } - } - if (this.status !== 'any') { - query.query.status = this.status - } - this.$router.replace(query) - }, 500), - fetchData: _.debounce(function () { - var self = this - this.isLoading = true - let url = FETCH_URL - let params = { - page: this.page, - page_size: this.paginateBy, - q: this.query, - ordering: this.getOrderingAsString() - } - if (this.status !== 'any') { - params.status = this.status - } - logger.default.debug('Fetching request...') - axios.get(url, {params: params}).then((response) => { - self.result = response.data - self.isLoading = false - }) - }, 500), - selectPage: function (page) { - this.page = page - } - }, - computed: { - orderingOptions: function () { - return [ - ['creation_date', this.$t('Creation date')], - ['artist_name', this.$t('Artist name')], - ['user__username', this.$t('User')] - ] - } - }, - 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() - }, - status () { - this.updateQueryString() - this.fetchData() - } - } -} -</script> - -<!-- Add "scoped" attribute to limit CSS to this component only --> -<style scoped> -</style> diff --git a/front/src/router/index.js b/front/src/router/index.js index 19474376..bb59b534 100644 --- a/front/src/router/index.js +++ b/front/src/router/index.js @@ -24,13 +24,13 @@ import RadioBuilder from '@/components/library/radios/Builder' import RadioDetail from '@/views/radios/Detail' import BatchList from '@/components/library/import/BatchList' import BatchDetail from '@/components/library/import/BatchDetail' -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 AdminLibraryBase from '@/views/admin/library/Base' import AdminLibraryFilesList from '@/views/admin/library/FilesList' +import AdminLibraryRequestsList from '@/views/admin/library/RequestsList' import AdminUsersBase from '@/views/admin/users/Base' import AdminUsersDetail from '@/views/admin/users/UsersDetail' import AdminUsersList from '@/views/admin/users/UsersList' @@ -184,6 +184,11 @@ export default new Router({ path: 'files', name: 'manage.library.files', component: AdminLibraryFilesList + }, + { + path: 'requests', + name: 'manage.library.requests', + component: AdminLibraryRequestsList } ] }, @@ -278,21 +283,7 @@ export default new Router({ children: [ ] }, - { path: 'import/batches/:id', name: 'library.import.batches.detail', component: BatchDetail, props: true }, - { - path: 'requests/', - name: 'library.requests', - component: RequestsList, - props: (route) => ({ - defaultOrdering: route.query.ordering, - defaultQuery: route.query.query, - defaultPaginateBy: route.query.paginateBy, - defaultPage: route.query.page, - defaultStatus: route.query.status || 'any' - }), - children: [ - ] - } + { path: 'import/batches/:id', name: 'library.import.batches.detail', component: BatchDetail, props: true } ] }, { path: '*', component: PageNotFound } diff --git a/front/src/views/admin/library/Base.vue b/front/src/views/admin/library/Base.vue index 834fca92..cc26c8d6 100644 --- a/front/src/views/admin/library/Base.vue +++ b/front/src/views/admin/library/Base.vue @@ -4,6 +4,15 @@ <router-link class="ui item" :to="{name: 'manage.library.files'}">{{ $t('Files') }}</router-link> + <router-link + class="ui item" + :to="{name: 'manage.library.requests'}"> + {{ $t('Import requests') }} + <div + :class="['ui', {'teal': $store.state.ui.notifications.importRequests > 0}, 'label']" + :title="$t('Pending import requests')"> + {{ $store.state.ui.notifications.importRequests }}</div> + </router-link> </div> <router-view :key="$route.fullPath"></router-view> </div> diff --git a/front/src/views/admin/library/RequestsList.vue b/front/src/views/admin/library/RequestsList.vue new file mode 100644 index 00000000..160bf890 --- /dev/null +++ b/front/src/views/admin/library/RequestsList.vue @@ -0,0 +1,23 @@ +<template> + <div v-title="$t('Import requests')"> + <div class="ui vertical stripe segment"> + <h2 class="ui header">{{ $t('Import requests') }}</h2> + <div class="ui hidden divider"></div> + <library-requests-table></library-requests-table> + </div> + </div> +</template> + +<script> +import LibraryRequestsTable from '@/components/manage/library/RequestsTable' + +export default { + components: { + LibraryRequestsTable + } +} +</script> + +<!-- Add "scoped" attribute to limit CSS to this component only --> +<style scoped> +</style> -- GitLab