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