From e9e466bcb5c1d089401ef852a59ec77c068e110f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Ciar=C3=A1n=20Ainsworth?= <ciaranainsworth@posteo.net>
Date: Thu, 17 Oct 2019 14:15:33 +0200
Subject: [PATCH] Added placeholders across the application

---
 changes/changelog.d/750.enhancement           |   1 +
 front/src/components/audio/album/Widget.vue   |   9 +-
 front/src/components/audio/track/Widget.vue   |  13 +-
 front/src/components/favorites/List.vue       |  15 +-
 front/src/components/library/Albums.vue       |  17 +
 front/src/components/library/Artists.vue      |  17 +
 front/src/components/library/Radios.vue       |  19 +-
 .../manage/moderation/DomainsTable.vue        |   8 +-
 .../playlists/PlaceholderWidget.vue           |  18 -
 .../components/playlists/PlaylistModal.vue    |  12 +
 front/src/components/playlists/Widget.vue     |  25 +-
 .../views/content/libraries/FilesTable.vue    | 313 +++++++++++-------
 front/src/views/playlists/Detail.vue          |  14 +-
 front/src/views/playlists/List.vue            |  19 +-
 front/src/views/radios/Detail.vue             |  17 +-
 15 files changed, 366 insertions(+), 151 deletions(-)
 create mode 100644 changes/changelog.d/750.enhancement
 delete mode 100644 front/src/components/playlists/PlaceholderWidget.vue

diff --git a/changes/changelog.d/750.enhancement b/changes/changelog.d/750.enhancement
new file mode 100644
index 000000000..5c66ad162
--- /dev/null
+++ b/changes/changelog.d/750.enhancement
@@ -0,0 +1 @@
+Placeholders will now be shown if no content is available across the application (#750)
\ No newline at end of file
diff --git a/front/src/components/audio/album/Widget.vue b/front/src/components/audio/album/Widget.vue
index c9e395f3c..e5ee7f742 100644
--- a/front/src/components/audio/album/Widget.vue
+++ b/front/src/components/audio/album/Widget.vue
@@ -35,7 +35,14 @@
         </div>
       </div>
     </div>
-    <div v-if="!isLoading && albums.length === 0">No results matching your query.</div>
+    <template v-if="!isLoading && albums.length === 0">
+      <div class="ui placeholder segment">
+        <div class="ui icon header">
+          <i class="compact disc icon"></i>
+          No results matching your query
+        </div>
+      </div>
+    </template>
   </div>
 </template>
 
diff --git a/front/src/components/audio/track/Widget.vue b/front/src/components/audio/track/Widget.vue
index 788d279d0..235659564 100644
--- a/front/src/components/audio/track/Widget.vue
+++ b/front/src/components/audio/track/Widget.vue
@@ -7,7 +7,7 @@
     <button :disabled="!previousPage" @click="fetchData(previousPage)" :class="['ui', {disabled: !previousPage}, 'circular', 'icon', 'basic', 'button']"><i :class="['ui', 'angle up', 'icon']"></i></button>
     <button :disabled="!nextPage" @click="fetchData(nextPage)" :class="['ui', {disabled: !nextPage}, 'circular', 'icon', 'basic', 'button']"><i :class="['ui', 'angle down', 'icon']"></i></button>
     <button @click="fetchData(url)" :class="['ui', 'circular', 'icon', 'basic', 'button']"><i :class="['ui', 'refresh', 'icon']"></i></button>
-    <div class="ui divided unstackable items">
+    <div v-if="count > 0" class="ui divided unstackable items">
       <div :class="['item', itemClasses]" v-for="object in objects" :key="object.id">
         <div class="ui tiny image">
           <img v-if="object.track.album.cover.original" v-lazy="$store.getters['instance/absoluteUrl'](object.track.album.cover.medium_square_crop)">
@@ -51,6 +51,17 @@
         <div class="ui loader"></div>
       </div>
     </div>
+    <div v-else class="ui placeholder segment">
+      <div class="ui icon header">
+        <i class="music icon"></i>
+        <translate translate-context="Content/Home/Placeholder">
+          Nothing found
+        </translate>
+      </div>
+      <div v-if="isLoading" class="ui inverted active dimmer">
+        <div class="ui loader"></div>
+      </div>
+    </div>
   </div>
 </template>
 
diff --git a/front/src/components/favorites/List.vue b/front/src/components/favorites/List.vue
index 6402d417f..f7d37369c 100644
--- a/front/src/components/favorites/List.vue
+++ b/front/src/components/favorites/List.vue
@@ -18,7 +18,7 @@
       </h2>
       <radio-button v-if="hasFavorites" type="favorites"></radio-button>
     </section>
-    <section class="ui vertical stripe segment">
+    <section v-if="hasFavorites" class="ui vertical stripe segment">
       <div :class="['ui', {'loading': isLoading}, 'form']">
         <div class="fields">
           <div class="field">
@@ -46,7 +46,6 @@
           </div>
         </div>
       </div>
-
       <track-table v-if="results" :tracks="results.results"></track-table>
       <div class="ui center aligned basic segment">
         <pagination
@@ -58,6 +57,18 @@
           ></pagination>
       </div>
     </section>
+    <div v-else class="ui placeholder segment">
+      <div class="ui icon header">
+        <i class="broken heart icon"></i>
+        <translate
+          translate-context="Content/Home/Placeholder"
+        >No tracks have been added to your favorites yet</translate>
+      </div>
+      <router-link :to="'/library'" class="ui green labeled icon button">
+      <i class="headphones icon"></i>
+        <translate translate-context="Content/*/Verb">Browse the library</translate>
+      </router-link>
+    </div>
   </main>
 </template>
 
diff --git a/front/src/components/library/Albums.vue b/front/src/components/library/Albums.vue
index 7aad836f0..07e20b22e 100644
--- a/front/src/components/library/Albums.vue
+++ b/front/src/components/library/Albums.vue
@@ -59,6 +59,23 @@
             :key="album.id"
             :album="album"></album-card>
         </div>
+        <div v-else class="ui placeholder segment sixteen wide column" style="text-align: center; display: flex; align-items: center">
+          <div class="ui icon header">
+            <i class="compact disc icon"></i>
+            <translate translate-context="Content/Albums/Placeholder">
+              No results matching your query
+            </translate>
+          </div>
+          <router-link
+          v-if="$store.state.auth.authenticated"
+          :to="{name: 'content.index'}"
+          class="ui green button labeled icon">
+          <i class="upload icon"></i>
+            <translate translate-context="Content/*/Verb">
+              Add some music
+            </translate>
+          </router-link>
+        </div>
       </div>
       <div class="ui center aligned basic segment">
         <pagination
diff --git a/front/src/components/library/Artists.vue b/front/src/components/library/Artists.vue
index 2ac23ac8e..a56abac14 100644
--- a/front/src/components/library/Artists.vue
+++ b/front/src/components/library/Artists.vue
@@ -48,6 +48,23 @@
         </div>
         <artist-card :artist="artist" v-for="artist in result.results" :key="artist.id"></artist-card>
       </div>
+      <div v-else-if="!isLoading" class="ui placeholder segment sixteen wide column" style="text-align: center; display: flex; align-items: center">
+        <div class="ui icon header">
+          <i class="compact disc icon"></i>
+          <translate translate-context="Content/Artists/Placeholder">
+            No results matching your query
+          </translate>
+        </div>
+        <router-link
+          v-if="$store.state.auth.authenticated"
+          :to="{name: 'content.index'}"
+          class="ui green button labeled icon">
+          <i class="upload icon"></i>
+          <translate translate-context="Content/*/Verb">
+              Add some music
+          </translate>
+        </router-link>
+      </div>
       <div class="ui center aligned basic segment">
         <pagination
           v-if="result && result.count > paginateBy"
diff --git a/front/src/components/library/Radios.vue b/front/src/components/library/Radios.vue
index 2d68d9072..a646321bc 100644
--- a/front/src/components/library/Radios.vue
+++ b/front/src/components/library/Radios.vue
@@ -60,6 +60,23 @@
         </div>
       </div>
       <div class="ui hidden divider"></div>
+      <div v-if="result && !result.results.length > 0" class="ui placeholder segment">
+        <div class="ui icon header">
+          <i class="feed icon"></i>
+          <translate translate-context="Content/Radios/Placeholder">
+            No results matching your query
+          </translate>
+        </div>
+        <router-link
+        v-if="$store.state.auth.authenticated"
+        :to="{name: 'library.radios.build'}"
+        class="ui green button labeled icon">
+          <i class="rss icon"></i>
+          <translate translate-context="Content/*/Verb">
+            Create a radio
+          </translate>
+        </router-link>
+      </div>
       <div
         v-if="result"
         v-masonry
@@ -76,7 +93,7 @@
             v-for="radio in result.results"
             :key="radio.id"
             :custom-radio="radio"></radio-card>
-        </div>
+        </div>        
       </div>
       <div class="ui center aligned basic segment">
         <pagination
diff --git a/front/src/components/manage/moderation/DomainsTable.vue b/front/src/components/manage/moderation/DomainsTable.vue
index 4b7d20739..ef83fb916 100644
--- a/front/src/components/manage/moderation/DomainsTable.vue
+++ b/front/src/components/manage/moderation/DomainsTable.vue
@@ -1,5 +1,5 @@
 <template>
-  <div>
+  <div v-if="result.count > 0">
     <div class="ui inline form">
       <div class="fields">
         <div class="ui field">
@@ -90,6 +90,12 @@
       </span>
     </div>
   </div>
+  <div v-else class="ui placeholder segment">
+    <div class="ui icon header">
+      <i class="server icon"></i>
+      <translate translate-context="Content/Home/Placeholder">No interactions with other pods yet</translate>
+    </div>
+  </div>
 </template>
 
 <script>
diff --git a/front/src/components/playlists/PlaceholderWidget.vue b/front/src/components/playlists/PlaceholderWidget.vue
deleted file mode 100644
index 772fd6c9d..000000000
--- a/front/src/components/playlists/PlaceholderWidget.vue
+++ /dev/null
@@ -1,18 +0,0 @@
-<template>
-  <div class="ui placeholder segment">
-    <div class="ui icon header">
-      <i class="list icon"></i>
-      <translate translate-context="Content/Home/Placeholder">
-        No playlists have been created yet
-      </translate>
-    </div>
-    <button
-      @click="$store.commit('playlists/chooseTrack', null)"
-      class="ui primary button"
-      >
-      <translate translate-context="Content/Home/CreatePlaylist">
-        Create Playlist
-      </translate>
-    </button>
-  </div>
-</template>
diff --git a/front/src/components/playlists/PlaylistModal.vue b/front/src/components/playlists/PlaylistModal.vue
index 9fb77d875..714d9feba 100644
--- a/front/src/components/playlists/PlaylistModal.vue
+++ b/front/src/components/playlists/PlaylistModal.vue
@@ -38,6 +38,7 @@
           </ul>
         </div>
         </div>
+        <div v-if="playlists.length > 0">
         <h4 class="ui header"><translate translate-context="Popup/Playlist/Title">Available playlists</translate></h4>
         <table class="ui unstackable very basic table">
           <thead>
@@ -72,6 +73,17 @@
             </tr>
           </tbody>
         </table>
+        </div>
+        <template v-else>
+          <div class="ui placeholder segment">
+            <div class="ui icon header">
+              <i class="list icon"></i>
+              <translate translate-context="Content/Home/Placeholder">
+                No playlists have been created yet
+              </translate>
+            </div>
+          </div>
+        </template>
       </div>
     </div>
     <div class="actions">
diff --git a/front/src/components/playlists/Widget.vue b/front/src/components/playlists/Widget.vue
index 9cd1c1d16..c9094d537 100644
--- a/front/src/components/playlists/Widget.vue
+++ b/front/src/components/playlists/Widget.vue
@@ -12,9 +12,24 @@
     <template v-if="playlistsExist">
       <playlist-card v-for="playlist in objects" :key="playlist.id" :playlist="playlist"></playlist-card>
     </template>
-    <template v-else>
-      <placeholder-widget></placeholder-widget>
-    </template>
+    <div v-else class="ui placeholder segment">
+      <div class="ui icon header">
+        <i class="list icon"></i>
+        <translate translate-context="Content/Home/Placeholder">
+          No playlists have been created yet
+        </translate>
+      </div>
+      <button
+        v-if="$store.state.auth.authenticated"
+        @click="$store.commit('playlists/chooseTrack', null)"
+        class="ui green icon labeled button"
+        >
+        <i class="list icon"></i>
+        <translate translate-context="Content/Home/CreatePlaylist">
+          Create Playlist
+        </translate>
+      </button>
+    </div>
   </div>
 </template>
 
@@ -22,7 +37,6 @@
 import _ from '@/lodash'
 import axios from 'axios'
 import PlaylistCard from '@/components/playlists/Card'
-import PlaceholderWidget from '@/components/playlists/PlaceholderWidget'
 
 export default {
   props: {
@@ -30,8 +44,7 @@ export default {
     url: {type: String, required: true}
   },
   components: {
-    PlaylistCard,
-    PlaceholderWidget
+    PlaylistCard
   },
   data () {
     return {
diff --git a/front/src/views/content/libraries/FilesTable.vue b/front/src/views/content/libraries/FilesTable.vue
index 4e67eb180..fd0348a6e 100644
--- a/front/src/views/content/libraries/FilesTable.vue
+++ b/front/src/views/content/libraries/FilesTable.vue
@@ -3,34 +3,67 @@
     <div class="ui inline form">
       <div class="fields">
         <div class="ui six wide field">
-          <label><translate translate-context="Content/Search/Input.Label/Noun">Search</translate></label>
+          <label>
+            <translate translate-context="Content/Search/Input.Label/Noun">Search</translate>
+          </label>
           <form @submit.prevent="search.query = $refs.search.value">
-            <input name="search" ref="search" type="text" :value="search.query" :placeholder="labels.searchPlaceholder" />
+            <input
+              name="search"
+              ref="search"
+              type="text"
+              :value="search.query"
+              :placeholder="labels.searchPlaceholder"
+            />
           </form>
         </div>
         <div class="field">
-          <label><translate translate-context="Content/*/*/Noun">Import status</translate></label>
-          <select class="ui dropdown" @change="addSearchToken('status', $event.target.value)" :value="getTokenValue('status', '')">
-            <option value=""><translate translate-context="Content/*/Dropdown">All</translate></option>
-            <option value="pending"><translate translate-context="Content/Library/*/Short">Pending</translate></option>
-            <option value="skipped"><translate translate-context="Content/Library/*">Skipped</translate></option>
-            <option value="errored"><translate translate-context="Content/Library/Dropdown">Failed</translate></option>
-            <option value="finished"><translate translate-context="Content/Library/*">Finished</translate></option>
+          <label>
+            <translate translate-context="Content/*/*/Noun">Import status</translate>
+          </label>
+          <select
+            class="ui dropdown"
+            @change="addSearchToken('status', $event.target.value)"
+            :value="getTokenValue('status', '')"
+          >
+            <option value>
+              <translate translate-context="Content/*/Dropdown">All</translate>
+            </option>
+            <option value="pending">
+              <translate translate-context="Content/Library/*/Short">Pending</translate>
+            </option>
+            <option value="skipped">
+              <translate translate-context="Content/Library/*">Skipped</translate>
+            </option>
+            <option value="errored">
+              <translate translate-context="Content/Library/Dropdown">Failed</translate>
+            </option>
+            <option value="finished">
+              <translate translate-context="Content/Library/*">Finished</translate>
+            </option>
           </select>
         </div>
         <div class="field">
-          <label><translate translate-context="Content/Search/Dropdown.Label/Noun">Ordering</translate></label>
+          <label>
+            <translate translate-context="Content/Search/Dropdown.Label/Noun">Ordering</translate>
+          </label>
           <select class="ui dropdown" v-model="ordering">
-            <option v-for="option in orderingOptions" :value="option[0]">
-              {{ sharedLabels.filters[option[1]] }}
-            </option>
+            <option
+              v-for="option in orderingOptions"
+              :value="option[0]"
+            >{{ sharedLabels.filters[option[1]] }}</option>
           </select>
         </div>
         <div class="field">
-          <label><translate translate-context="Content/Search/Dropdown.Label/Noun">Ordering direction</translate></label>
+          <label>
+            <translate translate-context="Content/Search/Dropdown.Label/Noun">Ordering direction</translate>
+          </label>
           <select class="ui dropdown" v-model="orderingDirection">
-            <option value="+"><translate translate-context="Content/Search/Dropdown">Ascending</translate></option>
-            <option value="-"><translate translate-context="Content/Search/Dropdown">Descending</translate></option>
+            <option value="+">
+              <translate translate-context="Content/Search/Dropdown">Ascending</translate>
+            </option>
+            <option value="-">
+              <translate translate-context="Content/Search/Dropdown">Descending</translate>
+            </option>
           </select>
         </div>
       </div>
@@ -38,10 +71,18 @@
     <import-status-modal :upload="detailedUpload" :show.sync="showUploadDetailModal" />
     <div class="dimmable">
       <div v-if="isLoading" class="ui active inverted dimmer">
-          <div class="ui loader"></div>
+        <div class="ui loader"></div>
+      </div>
+      <div v-else-if="!result && result.results.length === 0 && !needsRefresh" class="ui placeholder segment">
+        <div class="ui icon header">
+          <i class="upload icon"></i>
+          <translate
+          translate-context="Content/Home/Placeholder"
+          >No tracks have been added to this library yet</translate>
+        </div>
       </div>
       <action-table
-        v-if="result"
+        v-else
         @action-launched="fetchData"
         :id-field="'uuid'"
         :objects-data="result"
@@ -51,15 +92,30 @@
         :needs-refresh="needsRefresh"
         :action-url="'uploads/action/'"
         @refresh="fetchData"
-        :filters="actionFilters">
+        :filters="actionFilters"
+      >
         <template slot="header-cells">
-          <th><translate translate-context="*/*/*/Noun">Title</translate></th>
-          <th><translate translate-context="*/*/*/Noun">Artist</translate></th>
-          <th><translate translate-context="*/*/*">Album</translate></th>
-          <th><translate translate-context="*/*/*/Noun">Upload date</translate></th>
-          <th><translate translate-context="Content/*/*/Noun">Import status</translate></th>
-          <th><translate translate-context="Content/*/*">Duration</translate></th>
-          <th><translate translate-context="Content/*/*/Noun">Size</translate></th>
+          <th>
+            <translate translate-context="*/*/*/Noun">Title</translate>
+          </th>
+          <th>
+            <translate translate-context="*/*/*/Noun">Artist</translate>
+          </th>
+          <th>
+            <translate translate-context="*/*/*">Album</translate>
+          </th>
+          <th>
+            <translate translate-context="*/*/*/Noun">Upload date</translate>
+          </th>
+          <th>
+            <translate translate-context="Content/*/*/Noun">Import status</translate>
+          </th>
+          <th>
+            <translate translate-context="Content/*/*">Duration</translate>
+          </th>
+          <th>
+            <translate translate-context="Content/*/*/Noun">Size</translate>
+          </th>
         </template>
         <template slot="row-cells" slot-scope="scope">
           <template v-if="scope.obj.track">
@@ -67,10 +123,18 @@
               <span :title="scope.obj.track.title">{{ scope.obj.track.title|truncate(25) }}</span>
             </td>
             <td>
-              <span class="discrete link" @click="addSearchToken('artist', scope.obj.track.artist.name)" :title="scope.obj.track.artist.name">{{ scope.obj.track.artist.name|truncate(20) }}</span>
+              <span
+                class="discrete link"
+                @click="addSearchToken('artist', scope.obj.track.artist.name)"
+                :title="scope.obj.track.artist.name"
+              >{{ scope.obj.track.artist.name|truncate(20) }}</span>
             </td>
             <td>
-              <span class="discrete link" @click="addSearchToken('album', scope.obj.track.album.title)" :title="scope.obj.track.album.title">{{ scope.obj.track.album.title|truncate(20) }}</span>
+              <span
+                class="discrete link"
+                @click="addSearchToken('album', scope.obj.track.album.title)"
+                :title="scope.obj.track.album.title"
+              >{{ scope.obj.track.album.title|truncate(20) }}</span>
             </td>
           </template>
           <template v-else>
@@ -82,22 +146,24 @@
             <human-date :date="scope.obj.creation_date"></human-date>
           </td>
           <td>
-            <span class="discrete link" @click="addSearchToken('status', scope.obj.import_status)" :title="sharedLabels.fields.import_status.choices[scope.obj.import_status].help">
-              {{ sharedLabels.fields.import_status.choices[scope.obj.import_status].label }}
-            </span>
-            <button class="ui tiny basic icon button" :title="sharedLabels.fields.import_status.detailTitle" @click="detailedUpload = scope.obj; showUploadDetailModal = true">
+            <span
+              class="discrete link"
+              @click="addSearchToken('status', scope.obj.import_status)"
+              :title="sharedLabels.fields.import_status.choices[scope.obj.import_status].help"
+            >{{ sharedLabels.fields.import_status.choices[scope.obj.import_status].label }}</span>
+            <button
+              class="ui tiny basic icon button"
+              :title="sharedLabels.fields.import_status.detailTitle"
+              @click="detailedUpload = scope.obj; showUploadDetailModal = true"
+            >
               <i class="question circle outline icon"></i>
             </button>
           </td>
-          <td v-if="scope.obj.duration">
-            {{ time.parse(scope.obj.duration) }}
-          </td>
+          <td v-if="scope.obj.duration">{{ time.parse(scope.obj.duration) }}</td>
           <td v-else>
             <translate translate-context="*/*/*">N/A</translate>
           </td>
-          <td v-if="scope.obj.size">
-            {{ scope.obj.size | humanSize }}
-          </td>
+          <td v-if="scope.obj.size">{{ scope.obj.size | humanSize }}</td>
           <td v-else>
             <translate translate-context="*/*/*">N/A</translate>
           </td>
@@ -112,44 +178,50 @@
         :current="page"
         :paginate-by="paginateBy"
         :total="result.count"
-        ></pagination>
+      ></pagination>
 
       <span v-if="result && result.results.length > 0">
-        <translate translate-context="Content/*/Paragraph"
-          :translate-params="{start: ((page-1) * paginateBy) + 1, end: ((page-1) * paginateBy) + result.results.length, total: result.count}">
-          Showing results %{ start }-%{ end } on %{ total }
-        </translate>
+        <translate
+          translate-context="Content/*/Paragraph"
+          :translate-params="{start: ((page-1) * paginateBy) + 1, end: ((page-1) * paginateBy) + result.results.length, total: result.count}"
+        >Showing results %{ start }-%{ end } on %{ total }</translate>
       </span>
     </div>
   </div>
 </template>
 
 <script>
-import axios from 'axios'
-import _ from '@/lodash'
-import time from '@/utils/time'
-import {normalizeQuery, parseTokens} from '@/search'
+import axios from "axios";
+import _ from "@/lodash";
+import time from "@/utils/time";
+import { normalizeQuery, parseTokens } from "@/search";
 
-import Pagination from '@/components/Pagination'
-import ActionTable from '@/components/common/ActionTable'
-import OrderingMixin from '@/components/mixins/Ordering'
-import TranslationsMixin from '@/components/mixins/Translations'
-import SmartSearchMixin from '@/components/mixins/SmartSearch'
-import ImportStatusModal from '@/components/library/ImportStatusModal'
+import Pagination from "@/components/Pagination";
+import ActionTable from "@/components/common/ActionTable";
+import OrderingMixin from "@/components/mixins/Ordering";
+import TranslationsMixin from "@/components/mixins/Translations";
+import SmartSearchMixin from "@/components/mixins/SmartSearch";
+import ImportStatusModal from "@/components/library/ImportStatusModal";
 
 export default {
   mixins: [OrderingMixin, TranslationsMixin, SmartSearchMixin],
   props: {
-    filters: {type: Object, required: false},
-    needsRefresh: {type: Boolean, required: false, default: false},
-    customObjects: {type: Array, required: false, default: () => { return [] }}
+    filters: { type: Object, required: false },
+    needsRefresh: { type: Boolean, required: false, default: false },
+    customObjects: {
+      type: Array,
+      required: false,
+      default: () => {
+        return [];
+      }
+    }
   },
   components: {
     Pagination,
     ActionTable,
     ImportStatusModal
   },
-  data () {
+  data() {
     return {
       time,
       detailedUpload: null,
@@ -162,100 +234,109 @@ export default {
         query: this.defaultQuery,
         tokens: parseTokens(normalizeQuery(this.defaultQuery))
       },
-      orderingDirection: '-',
-      ordering: 'creation_date',
+      orderingDirection: "-",
+      ordering: "creation_date",
       orderingOptions: [
-        ['creation_date', 'creation_date'],
-        ['title', 'track_title'],
-        ['size', 'size'],
-        ['duration', 'duration'],
-        ['bitrate', 'bitrate'],
-        ['album_title', 'album_title'],
-        ['artist_name', 'artist_name']
+        ["creation_date", "creation_date"],
+        ["title", "track_title"],
+        ["size", "size"],
+        ["duration", "duration"],
+        ["bitrate", "bitrate"],
+        ["album_title", "album_title"],
+        ["artist_name", "artist_name"]
       ]
-    }
+    };
   },
-  created () {
-    this.fetchData()
+  created() {
+    this.fetchData();
   },
   methods: {
-    fetchData () {
-      this.$emit('fetch-start')
-      let params = _.merge({
-        'page': this.page,
-        'page_size': this.paginateBy,
-        'ordering': this.getOrderingAsString(),
-        'q': this.search.query
-      }, this.filters || {})
-      let self = this
-      self.isLoading = true
-      self.checked = []
-      axios.get('/uploads/', {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
+    fetchData() {
+      this.$emit("fetch-start");
+      let params = _.merge(
+        {
+          page: this.page,
+          page_size: this.paginateBy,
+          ordering: this.getOrderingAsString(),
+          q: this.search.query
+        },
+        this.filters || {}
+      );
+      let self = this;
+      self.isLoading = true;
+      self.checked = [];
+      axios.get("/uploads/", { params: params }).then(
+        response => {
+          self.result = response.data;
+          self.isLoading = false;
+        },
+        error => {
+          self.isLoading = false;
+          self.errors = error.backendErrors;
+        }
+      );
     },
   },
   computed: {
-    labels () {
+    labels() {
       return {
-        searchPlaceholder: this.$pgettext('Content/Library/Input.Placeholder', 'Search by title, artist, album…'),
-      }
+        searchPlaceholder: this.$pgettext(
+          "Content/Library/Input.Placeholder",
+          "Search by title, artist, album…"
+        )
+      };
     },
-    actionFilters () {
+    actionFilters() {
       var currentFilters = {
         q: this.search.query
-      }
+      };
       if (this.filters) {
-        return _.merge(currentFilters, this.filters)
+        return _.merge(currentFilters, this.filters);
       } else {
-        return currentFilters
+        return currentFilters;
       }
     },
-    actions () {
-      let deleteMsg = this.$pgettext('*/*/*/Verb', 'Delete')
-      let relaunchMsg = this.$pgettext('Content/Library/Dropdown/Verb', 'Restart import')
+    actions() {
+      let deleteMsg = this.$pgettext("*/*/*/Verb", "Delete");
+      let relaunchMsg = this.$pgettext(
+        "Content/Library/Dropdown/Verb",
+        "Restart import"
+      );
       return [
         {
-          name: 'delete',
+          name: "delete",
           label: deleteMsg,
           isDangerous: true,
           allowAll: true
         },
         {
-          name: 'relaunch_import',
+          name: "relaunch_import",
           label: relaunchMsg,
           isDangerous: true,
           allowAll: true,
           filterCheckable: f => {
-            return f.import_status != 'finished'
+            return f.import_status != "finished";
           }
         }
-      ]
+      ];
     }
   },
   watch: {
-    orderingDirection: function () {
-      this.page = 1
-      this.fetchData()
+    orderingDirection: function() {
+      this.page = 1;
+      this.fetchData();
     },
-    page: function () {
-      this.fetchData()
+    page: function() {
+      this.fetchData();
     },
-    ordering: function () {
-      this.page = 1
-      this.fetchData()
+    ordering: function() {
+      this.page = 1;
+      this.fetchData();
     },
-    search (newValue) {
-      this.page = 1
-      this.fetchData()
+    search(newValue) {
+      this.page = 1;
+      this.fetchData();
     }
   }
-}
+};
 </script>
diff --git a/front/src/views/playlists/Detail.vue b/front/src/views/playlists/Detail.vue
index 3b6b9000b..5dc99e78b 100644
--- a/front/src/views/playlists/Detail.vue
+++ b/front/src/views/playlists/Detail.vue
@@ -55,7 +55,6 @@
         <div class="content">
           <div class="description">
             <embed-wizard type="playlist" :id="playlist.id" />
-
           </div>
         </div>
         <div class="actions">
@@ -64,7 +63,6 @@
           </div>
         </div>
       </modal>
-
     </section>
     <section class="ui vertical stripe segment">
       <template v-if="edit">
@@ -73,10 +71,20 @@
           @tracks-updated="updatePlts"
           :playlist="playlist" :playlist-tracks="playlistTracks"></playlist-editor>
       </template>
-      <template v-else>
+      <template v-else-if="tracks.length > 0">
         <h2><translate translate-context="*/*/*">Tracks</translate></h2>
         <track-table :display-position="true" :tracks="tracks"></track-table>
       </template>
+      <div v-else class="ui placeholder segment">
+        <div class="ui icon header">
+          <i class="list icon"></i>
+          <translate translate-context="Content/Home/Placeholder">There are no tracks in this playlist yet</translate>
+        </div>
+        <button @click="edit = !edit" class="ui green icon labeled button">
+          <i class="pencil icon"></i>
+          <translate translate-context="Content/Home/CreatePlaylist">Edit</translate>
+        </button>
+      </div>
     </section>
   </main>
 </template>
diff --git a/front/src/views/playlists/List.vue b/front/src/views/playlists/List.vue
index 1ff56b5d5..9fb53fd66 100644
--- a/front/src/views/playlists/List.vue
+++ b/front/src/views/playlists/List.vue
@@ -40,7 +40,24 @@
         </div>
       </div>
       <div class="ui hidden divider"></div>
-      <playlist-card-list v-if="result" :playlists="result.results"></playlist-card-list>
+      <playlist-card-list v-if="result && result.results.length > 0" :playlists="result.results"></playlist-card-list>
+      <div v-else-if="result && !result.results.length > 0" class="ui placeholder segment sixteen wide column" style="text-align: center; display: flex; align-items: center">
+        <div class="ui icon header">
+          <i class="list icon"></i>
+          <translate translate-context="Content/Playlists/Placeholder">
+            No results matching your query
+          </translate>
+        </div>
+        <button
+        v-if="$store.state.auth.authenticated"
+        @click="$store.commit('playlists/chooseTrack', null)"
+        class="ui green button labeled icon">
+        <i class="list icon"></i>
+        <translate translate-context="Content/*/Verb">
+          Create a playlist
+          </translate>
+        </button>
+      </div>
       <div class="ui center aligned basic segment">
         <pagination
           v-if="result && result.results.length > 0"
diff --git a/front/src/views/radios/Detail.vue b/front/src/views/radios/Detail.vue
index 355ed8e84..619ac754b 100644
--- a/front/src/views/radios/Detail.vue
+++ b/front/src/views/radios/Detail.vue
@@ -31,7 +31,7 @@
         </template>
       </div>
     </section>
-    <section class="ui vertical stripe segment">
+    <section v-if="totalTracks > 0" class="ui vertical stripe segment">
       <h2><translate translate-context="*/*/*">Tracks</translate></h2>
       <track-table :tracks="tracks"></track-table>
       <div class="ui center aligned basic segment">
@@ -44,6 +44,21 @@
           ></pagination>
       </div>
     </section>
+    <div v-else-if="!isLoading && !totalTracks > 0" class="ui placeholder segment">
+      <div class="ui icon header">
+        <i class="rss icon"></i>
+        <translate
+        translate-context="Content/Radios/Placeholder"
+        >No tracks have been added to this radio yet</translate>
+      </div>
+      <router-link
+      v-if="$store.state.auth.username === radio.user.username"
+      class="ui green icon labeled button" 
+      :to="{name: 'library.radios.edit', params: {id: radio.id}}" exact>
+      <i class="pencil icon"></i>
+        Edit…
+      </router-link>
+    </div>
   </main>
 </template>
 
-- 
GitLab