Added placeholders across the application

environments/review-front-594-ofn00k/deployments/3014
Ciarán Ainsworth 2019-10-17 14:15:33 +02:00
rodzic 860522a291
commit 11d6c7cf1d
15 zmienionych plików z 369 dodań i 154 usunięć

Wyświetl plik

@ -0,0 +1 @@
Placeholders will now be shown if no content is available across the application (#750)

Wyświetl plik

@ -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>

Wyświetl plik

@ -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>

Wyświetl plik

@ -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>

Wyświetl plik

@ -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

Wyświetl plik

@ -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"

Wyświetl plik

@ -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

Wyświetl plik

@ -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>

Wyświetl plik

@ -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>

Wyświetl plik

@ -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">

Wyświetl plik

@ -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 {

Wyświetl plik

@ -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>
</select>
</div>
<div class="field">
<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]] }}
<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 direction</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>
</select>
</div>
<div class="field">
<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>

Wyświetl plik

@ -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>

Wyświetl plik

@ -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"

Wyświetl plik

@ -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>