Commit 9ce24284 authored by Ciarán Ainsworth's avatar Ciarán Ainsworth
Browse files

Merge branch '750-do-not-display-empty-tables-headers' into 'develop'

Added placeholders across the application

Closes #750

See merge request funkwhale/funkwhale!922
parents 860522a2 11d6c7cf
Placeholders will now be shown if no content is available across the application (#750)
\ No newline at end of file
......@@ -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>
......
......@@ -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>
......
......@@ -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>
......
......@@ -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
......
......@@ -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"
......
......@@ -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
......
<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>
......
<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>
......@@ -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">
......
......@@ -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 {
......
......@@ -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() {