refactor(front): use section with title prop and pagination on all widgets
This commit is contained in:
parent
e09d0a20fa
commit
2e63cad388
|
@ -8,14 +8,15 @@ import { useI18n } from 'vue-i18n'
|
||||||
|
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
|
|
||||||
import AlbumCard from '~/components/album/Card.vue'
|
import usePage from '~/composables/navigation/usePage'
|
||||||
import Button from '~/components/ui/Button.vue'
|
|
||||||
import Layout from '~/components/ui/Layout.vue'
|
|
||||||
import Spacer from '~/components/ui/Spacer.vue'
|
|
||||||
import Loader from '~/components/ui/Loader.vue'
|
|
||||||
|
|
||||||
import useErrorHandler from '~/composables/useErrorHandler'
|
import useErrorHandler from '~/composables/useErrorHandler'
|
||||||
|
|
||||||
|
import AlbumCard from '~/components/album/Card.vue'
|
||||||
|
import Section from '~/components/ui/Section.vue'
|
||||||
|
import Loader from '~/components/ui/Loader.vue'
|
||||||
|
import Pagination from '~/components/ui/Pagination.vue'
|
||||||
|
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
@ -23,6 +24,7 @@ interface Props {
|
||||||
showCount?: boolean
|
showCount?: boolean
|
||||||
search?: boolean
|
search?: boolean
|
||||||
limit?: number
|
limit?: number
|
||||||
|
title?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
@ -36,6 +38,7 @@ const store = useStore()
|
||||||
const query = ref('')
|
const query = ref('')
|
||||||
const albums = reactive([] as Album[])
|
const albums = reactive([] as Album[])
|
||||||
const count = ref(0)
|
const count = ref(0)
|
||||||
|
const page = usePage()
|
||||||
const nextPage = ref()
|
const nextPage = ref()
|
||||||
|
|
||||||
const isLoading = ref(false)
|
const isLoading = ref(false)
|
||||||
|
@ -46,6 +49,7 @@ const fetchData = async (url = 'albums/') => {
|
||||||
const params = {
|
const params = {
|
||||||
q: query.value,
|
q: query.value,
|
||||||
...props.filters,
|
...props.filters,
|
||||||
|
page: page.value,
|
||||||
page_size: props.limit
|
page_size: props.limit
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -60,44 +64,39 @@ const fetchData = async (url = 'albums/') => {
|
||||||
isLoading.value = false
|
isLoading.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setTimeout(fetchData, 1000)
|
||||||
|
|
||||||
const performSearch = () => {
|
const performSearch = () => {
|
||||||
albums.length = 0
|
albums.length = 0
|
||||||
fetchData()
|
fetchData()
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => store.state.moderation.lastUpdate,
|
[() => store.state.moderation.lastUpdate, page],
|
||||||
() => fetchData(),
|
() => fetchData(),
|
||||||
{ immediate: true }
|
{ immediate: true }
|
||||||
)
|
)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="album-widget">
|
<Section
|
||||||
<h2
|
:h2="title"
|
||||||
v-if="!!$slots.title"
|
align-left
|
||||||
>
|
small-items
|
||||||
<slot name="title" />
|
>
|
||||||
<span
|
|
||||||
v-if="showCount"
|
|
||||||
class="ui tiny circular label"
|
|
||||||
>{{ count }}</span>
|
|
||||||
</h2>
|
|
||||||
<slot />
|
|
||||||
<inline-search-bar
|
<inline-search-bar
|
||||||
v-if="search"
|
v-if="search"
|
||||||
|
style="grid-column: 1 / -1;"
|
||||||
v-model="query"
|
v-model="query"
|
||||||
@search="performSearch"
|
@search="performSearch"
|
||||||
/>
|
/>
|
||||||
<Loader v-if="isLoading" />
|
<Loader v-if="isLoading" />
|
||||||
<template v-if="!isLoading && albums.length > 0">
|
<template v-if="!isLoading && albums.length > 0">
|
||||||
<Layout flex>
|
|
||||||
<album-card
|
<album-card
|
||||||
v-for="album in albums"
|
v-for="album in albums"
|
||||||
:key="album.id"
|
:key="album.id"
|
||||||
:album="album"
|
:album="album"
|
||||||
/>
|
/>
|
||||||
</Layout>
|
|
||||||
</template>
|
</template>
|
||||||
<slot
|
<slot
|
||||||
v-if="!isLoading && albums.length === 0"
|
v-if="!isLoading && albums.length === 0"
|
||||||
|
@ -106,17 +105,14 @@ watch(
|
||||||
<empty-state
|
<empty-state
|
||||||
:refresh="true"
|
:refresh="true"
|
||||||
@refresh="fetchData"
|
@refresh="fetchData"
|
||||||
|
style="grid-column: 1 / -1;"
|
||||||
/>
|
/>
|
||||||
</slot>
|
</slot>
|
||||||
<template v-if="nextPage">
|
<Pagination
|
||||||
<Spacer />
|
v-if="albums && count > props.limit"
|
||||||
<Button
|
v-model:page="page"
|
||||||
v-if="nextPage"
|
:pages="Math.ceil((count || 0) / props.limit)"
|
||||||
primary
|
style="grid-column: 1 / -1;"
|
||||||
@click="fetchData(nextPage)"
|
/>
|
||||||
>
|
</Section>
|
||||||
{{ t('components.audio.album.Widget.button.more') }}
|
|
||||||
</Button>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -1,25 +1,26 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { Artist } from '~/types'
|
import type { Artist } from '~/types'
|
||||||
|
|
||||||
import { reactive, ref, watch } from 'vue'
|
import { reactive, ref, watch, onMounted } from 'vue'
|
||||||
import { useStore } from '~/store'
|
import { useStore } from '~/store'
|
||||||
import { useI18n } from 'vue-i18n'
|
|
||||||
|
|
||||||
|
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
|
|
||||||
import ArtistCard from '~/components/artist/Card.vue'
|
|
||||||
import Button from '~/components/ui/Button.vue'
|
|
||||||
|
|
||||||
import useErrorHandler from '~/composables/useErrorHandler'
|
import useErrorHandler from '~/composables/useErrorHandler'
|
||||||
|
import usePage from '~/composables/navigation/usePage'
|
||||||
|
|
||||||
|
import ArtistCard from '~/components/artist/Card.vue'
|
||||||
|
import Section from '~/components/ui/Section.vue'
|
||||||
|
import Pagination from '~/components/ui/Pagination.vue'
|
||||||
|
|
||||||
const { t } = useI18n()
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
filters: Record<string, string | boolean>
|
filters: Record<string, string | boolean>
|
||||||
search?: boolean
|
search?: boolean
|
||||||
header?: boolean
|
header?: boolean
|
||||||
limit?: number
|
limit?: number
|
||||||
|
title?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
@ -33,6 +34,7 @@ const store = useStore()
|
||||||
const query = ref('')
|
const query = ref('')
|
||||||
const artists = reactive([] as Artist[])
|
const artists = reactive([] as Artist[])
|
||||||
const count = ref(0)
|
const count = ref(0)
|
||||||
|
const page = usePage()
|
||||||
const nextPage = ref()
|
const nextPage = ref()
|
||||||
|
|
||||||
const isLoading = ref(false)
|
const isLoading = ref(false)
|
||||||
|
@ -43,6 +45,7 @@ const fetchData = async (url = 'artists/') => {
|
||||||
const params = {
|
const params = {
|
||||||
q: query.value,
|
q: query.value,
|
||||||
...props.filters,
|
...props.filters,
|
||||||
|
page: page.value,
|
||||||
page_size: props.limit
|
page_size: props.limit
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -57,46 +60,31 @@ const fetchData = async (url = 'artists/') => {
|
||||||
isLoading.value = false
|
isLoading.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
setTimeout(fetchData, 1000)
|
||||||
|
})
|
||||||
|
|
||||||
const performSearch = () => {
|
const performSearch = () => {
|
||||||
artists.length = 0
|
artists.length = 0
|
||||||
fetchData()
|
fetchData()
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => store.state.moderation.lastUpdate,
|
[() => store.state.moderation.lastUpdate, page],
|
||||||
() => fetchData(),
|
() => fetchData(),
|
||||||
{ immediate: true }
|
{ immediate: true }
|
||||||
)
|
)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="wrapper">
|
<Section
|
||||||
<h3
|
align-left
|
||||||
v-if="header"
|
:h2="title"
|
||||||
class="ui header"
|
>
|
||||||
>
|
<Loader
|
||||||
<slot name="title" />
|
v-if="isLoading"
|
||||||
<span class="ui tiny circular label">{{ count }}</span>
|
style="grid-column: 1 / -1;"
|
||||||
</h3>
|
|
||||||
<inline-search-bar
|
|
||||||
v-if="search"
|
|
||||||
v-model="query"
|
|
||||||
@search="performSearch"
|
|
||||||
/>
|
/>
|
||||||
<div class="ui hidden divider" />
|
|
||||||
<div style="display:flex; flex-wrap:wrap; gap: 32px; margin-top:32px;">
|
|
||||||
<div
|
|
||||||
v-if="isLoading"
|
|
||||||
class="ui inverted active dimmer"
|
|
||||||
>
|
|
||||||
<div class="ui loader" />
|
|
||||||
</div>
|
|
||||||
<artist-card
|
|
||||||
v-for="artist in artists"
|
|
||||||
:key="artist.id"
|
|
||||||
:artist="artist"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<slot
|
<slot
|
||||||
v-if="!isLoading && artists.length === 0"
|
v-if="!isLoading && artists.length === 0"
|
||||||
name="empty-state"
|
name="empty-state"
|
||||||
|
@ -106,14 +94,21 @@ watch(
|
||||||
@refresh="fetchData"
|
@refresh="fetchData"
|
||||||
/>
|
/>
|
||||||
</slot>
|
</slot>
|
||||||
<template v-if="nextPage">
|
<inline-search-bar
|
||||||
<div class="ui hidden divider" />
|
v-if="!isLoading && search"
|
||||||
<Button
|
v-model="query"
|
||||||
v-if="nextPage"
|
@search="performSearch"
|
||||||
@click="fetchData(nextPage)"
|
style="grid-column: 1 / -1;"
|
||||||
>
|
/>
|
||||||
{{ t('components.audio.artist.Widget.button.more') }}
|
<artist-card
|
||||||
</Button>
|
v-for="artist in artists"
|
||||||
</template>
|
:key="artist.id"
|
||||||
</div>
|
:artist="artist"
|
||||||
|
/>
|
||||||
|
<Pagination
|
||||||
|
v-if="artists && count > limit"
|
||||||
|
v-model:page="page"
|
||||||
|
:pages="Math.ceil((count || 0) / limit)"
|
||||||
|
/>
|
||||||
|
</Section>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -59,33 +59,39 @@ const fetchData = async (url = 'channels/') => {
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
fetchData()
|
setTimeout(fetchData, 1000)
|
||||||
})
|
})
|
||||||
|
|
||||||
watch(() => [props.filters, page], () => {
|
watch([() => props.filters, page],
|
||||||
fetchData()
|
() => fetchData(),
|
||||||
}, { deep: true })
|
{ deep: true }
|
||||||
|
)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Section aling-left :h2="title" class="channel-widget">
|
<Section
|
||||||
<slot />
|
align-left
|
||||||
<Loader v-if="isLoading" />
|
:h2="title"
|
||||||
|
>
|
||||||
|
<Loader v-if="isLoading" style="grid-column: 1 / -1;" />
|
||||||
|
<template
|
||||||
|
v-if="!isLoading && channels.length === 0"
|
||||||
|
style="grid-column: 1 / -1;"
|
||||||
|
>
|
||||||
|
<empty-state
|
||||||
|
:refresh="true"
|
||||||
|
@refresh="fetchData('channels/')"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
<channel-card
|
<channel-card
|
||||||
v-for="object in channels"
|
v-for="object in channels"
|
||||||
:key="object.uuid"
|
:key="object.uuid"
|
||||||
:object="object"
|
:object="object"
|
||||||
/>
|
/>
|
||||||
</Section>
|
<Pagination
|
||||||
<template v-if="!isLoading && channels.length === 0">
|
v-if="channels && count > limit"
|
||||||
<empty-state
|
v-model:page="page"
|
||||||
:refresh="true"
|
:pages="Math.ceil((count || 0) / limit)"
|
||||||
@refresh="fetchData('channels/')"
|
|
||||||
/>
|
/>
|
||||||
</template>
|
</Section>
|
||||||
<Pagination
|
|
||||||
v-if="channels && count > limit"
|
|
||||||
v-model:page="page"
|
|
||||||
:pages="Math.ceil((count || 0) / limit)"
|
|
||||||
/>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -8,9 +8,9 @@ import { useI18n } from 'vue-i18n'
|
||||||
import { getArtistCoverUrl } from '~/utils/utils'
|
import { getArtistCoverUrl } from '~/utils/utils'
|
||||||
|
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
|
import usePage from '~/composables/navigation/usePage'
|
||||||
import useWebSocketHandler from '~/composables/useWebSocketHandler'
|
import useWebSocketHandler from '~/composables/useWebSocketHandler'
|
||||||
import Button from '~/components/ui/Button.vue'
|
|
||||||
import PlayButton from '~/components/audio/PlayButton.vue'
|
import PlayButton from '~/components/audio/PlayButton.vue'
|
||||||
import TagsList from '~/components/tags/List.vue'
|
import TagsList from '~/components/tags/List.vue'
|
||||||
import Section from '~/components/ui/Section.vue'
|
import Section from '~/components/ui/Section.vue'
|
||||||
|
@ -18,6 +18,7 @@ import Alert from '~/components/ui/Alert.vue'
|
||||||
import Spacer from '~/components/ui/Spacer.vue'
|
import Spacer from '~/components/ui/Spacer.vue'
|
||||||
import Loader from '~/components/ui/Loader.vue'
|
import Loader from '~/components/ui/Loader.vue'
|
||||||
import Heading from '~/components/ui/Heading.vue'
|
import Heading from '~/components/ui/Heading.vue'
|
||||||
|
import Pagination from '~/components/ui/Pagination.vue'
|
||||||
|
|
||||||
import useErrorHandler from '~/composables/useErrorHandler'
|
import useErrorHandler from '~/composables/useErrorHandler'
|
||||||
|
|
||||||
|
@ -30,16 +31,15 @@ interface Props {
|
||||||
filters: Record<string, string | boolean>
|
filters: Record<string, string | boolean>
|
||||||
url: string
|
url: string
|
||||||
isActivity?: boolean
|
isActivity?: boolean
|
||||||
showCount?: boolean
|
|
||||||
limit?: number
|
limit?: number
|
||||||
itemClasses?: string
|
itemClasses?: string
|
||||||
websocketHandlers?: string[]
|
websocketHandlers?: string[]
|
||||||
|
title?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const emit = defineEmits<Events>()
|
const emit = defineEmits<Events>()
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
isActivity: true,
|
isActivity: true,
|
||||||
showCount: false,
|
|
||||||
limit: 9,
|
limit: 9,
|
||||||
itemClasses: '',
|
itemClasses: '',
|
||||||
websocketHandlers: () => []
|
websocketHandlers: () => []
|
||||||
|
@ -50,7 +50,7 @@ const { t } = useI18n()
|
||||||
|
|
||||||
const objects = reactive([] as Listening[])
|
const objects = reactive([] as Listening[])
|
||||||
const count = ref(0)
|
const count = ref(0)
|
||||||
const nextPage = ref<string | null>(null)
|
const page = usePage()
|
||||||
|
|
||||||
const isLoading = ref(false)
|
const isLoading = ref(false)
|
||||||
|
|
||||||
|
@ -59,12 +59,12 @@ const fetchData = async (url = props.url) => {
|
||||||
|
|
||||||
const params = {
|
const params = {
|
||||||
...clone(props.filters),
|
...clone(props.filters),
|
||||||
page_size: props.limit
|
page: page.value,
|
||||||
|
page_size: props.limit ?? 9
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await axios.get(url, { params })
|
const response = await axios.get(url, { params })
|
||||||
nextPage.value = response.data.next
|
|
||||||
count.value = response.data.count
|
count.value = response.data.count
|
||||||
|
|
||||||
const newObjects = !props.isActivity
|
const newObjects = !props.isActivity
|
||||||
|
@ -80,11 +80,11 @@ const fetchData = async (url = props.url) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
fetchData()
|
setTimeout(fetchData, 1000)
|
||||||
})
|
})
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => store.state.moderation.lastUpdate,
|
[() => store.state.moderation.lastUpdate, page],
|
||||||
() => fetchData(),
|
() => fetchData(),
|
||||||
{ immediate: true }
|
{ immediate: true }
|
||||||
)
|
)
|
||||||
|
@ -113,18 +113,17 @@ watch(() => props.websocketHandlers.includes('Listen'), (to) => {
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<!-- TODO: Use activity.vue -->
|
<Section
|
||||||
<div class="track-widget">
|
:h2="title"
|
||||||
<h2 v-if="!!$slots.title">
|
medium-items
|
||||||
<slot name="title" />
|
align-left
|
||||||
<span
|
>
|
||||||
v-if="showCount"
|
<Loader v-if="isLoading"
|
||||||
class="ui tiny circular label"
|
style="grid-column: 1 / -1;"
|
||||||
>{{ count }}</span>
|
/>
|
||||||
</h2>
|
|
||||||
<Spacer :size="8" />
|
|
||||||
<Alert
|
<Alert
|
||||||
v-if="!isLoading && count === 0"
|
v-if="!isLoading && count === 0"
|
||||||
|
style="grid-column: 1 / -1;"
|
||||||
blue
|
blue
|
||||||
align-items="center"
|
align-items="center"
|
||||||
>
|
>
|
||||||
|
@ -133,103 +132,94 @@ watch(() => props.websocketHandlers.includes('Listen'), (to) => {
|
||||||
{{ t('components.audio.track.Widget.empty.noResults') }}
|
{{ t('components.audio.track.Widget.empty.noResults') }}
|
||||||
</h4>
|
</h4>
|
||||||
</Alert>
|
</Alert>
|
||||||
<Section
|
<!-- TODO: Use activity.vue -->
|
||||||
|
<div class="funkwhale activity"
|
||||||
v-if="count > 0"
|
v-if="count > 0"
|
||||||
medium-items
|
v-for="object in objects"
|
||||||
alignLeft
|
:key="object.id"
|
||||||
>
|
:class="['item', itemClasses]">
|
||||||
<div class="funkwhale activity"
|
<div class="activity-image">
|
||||||
v-for="object in objects"
|
<img
|
||||||
:key="object.id"
|
v-if="object.track.album && object.track.album.cover"
|
||||||
:class="['item', itemClasses]"
|
v-lazy="store.getters['instance/absoluteUrl'](object.track.album.cover.urls.medium_square_crop)"
|
||||||
>
|
alt=""
|
||||||
<div class="activity-image">
|
>
|
||||||
<img
|
<img
|
||||||
v-if="object.track.album && object.track.album.cover"
|
v-else-if="object.track.cover"
|
||||||
v-lazy="store.getters['instance/absoluteUrl'](object.track.album.cover.urls.medium_square_crop)"
|
v-lazy="store.getters['instance/absoluteUrl'](object.track.cover.urls.medium_square_crop)"
|
||||||
alt=""
|
alt=""
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
v-else-if="object.track.cover"
|
v-else-if="object.track.artist_credit && object.track.artist_credit.length > 1"
|
||||||
v-lazy="store.getters['instance/absoluteUrl'](object.track.cover.urls.medium_square_crop)"
|
v-lazy="getArtistCoverUrl(object.track.artist_credit)"
|
||||||
alt=""
|
alt=""
|
||||||
>
|
>
|
||||||
<img
|
<i
|
||||||
v-else-if="object.track.artist_credit && object.track.artist_credit.length > 1"
|
v-else
|
||||||
v-lazy="getArtistCoverUrl(object.track.artist_credit)"
|
class="bi bi-vinyl-fill"
|
||||||
alt=""
|
|
||||||
>
|
|
||||||
<i
|
|
||||||
v-else
|
|
||||||
class="bi bi-vinyl-fill"
|
|
||||||
/>
|
|
||||||
<!-- TODO: Add Playbutton overlay -->
|
|
||||||
</div>
|
|
||||||
<div class="activity-content">
|
|
||||||
<router-link
|
|
||||||
class="funkwhale link artist"
|
|
||||||
:to="{name: 'library.tracks.detail', params: {id: object.track.id}}"
|
|
||||||
>
|
|
||||||
<Heading :h3="object.track.title" title />
|
|
||||||
</router-link>
|
|
||||||
<Spacer :size="2"/>
|
|
||||||
<div
|
|
||||||
v-if="object.track.artist_credit"
|
|
||||||
class="funkwhale link artist"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
v-for="ac in object.track.artist_credit"
|
|
||||||
:key="ac.artist.id"
|
|
||||||
>
|
|
||||||
<router-link
|
|
||||||
class="discrete link"
|
|
||||||
:to="{ name: 'library.artists.detail', params: { id: ac.artist.id } }"
|
|
||||||
>
|
|
||||||
{{ ac.credit }}
|
|
||||||
</router-link>
|
|
||||||
<span v-if="ac.joinphrase">{{ ac.joinphrase }}</span>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<TagsList
|
|
||||||
label-classes="tiny"
|
|
||||||
:truncate-size="20"
|
|
||||||
:limit="2"
|
|
||||||
:show-more="false"
|
|
||||||
:tags="object.track.tags"
|
|
||||||
/>
|
|
||||||
<Spacer :size="4"/>
|
|
||||||
<div
|
|
||||||
v-if="isActivity"
|
|
||||||
class="extra"
|
|
||||||
>
|
|
||||||
<router-link
|
|
||||||
class="funkwhale link user"
|
|
||||||
:to="{name: 'profile.overview', params: {username: object.actor.name}}"
|
|
||||||
>
|
|
||||||
<span class="at symbol" />{{ object.actor.name }}
|
|
||||||
</router-link>
|
|
||||||
<span class="right floated"><human-date :date="object.creation_date" /></span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<play-button
|
|
||||||
:account="object.actor"
|
|
||||||
:dropdown-only="true"
|
|
||||||
:track="object.track"
|
|
||||||
square-small
|
|
||||||
/>
|
/>
|
||||||
|
<!-- TODO: Add Playbutton overlay -->
|
||||||
</div>
|
</div>
|
||||||
</Section>
|
<div class="activity-content">
|
||||||
<Loader v-if="isLoading" />
|
<router-link
|
||||||
<template v-if="nextPage">
|
class="funkwhale link artist"
|
||||||
<Spacer />
|
:to="{name: 'library.tracks.detail', params: {id: object.track.id}}"
|
||||||
<Button
|
>
|
||||||
primary
|
<Heading :h3="object.track.title" title />
|
||||||
@click="fetchData(nextPage as string)"
|
</router-link>
|
||||||
>
|
<Spacer :size="2"/>
|
||||||
{{ t('components.audio.track.Widget.button.more') }}
|
<div
|
||||||
</Button>
|
v-if="object.track.artist_credit"
|
||||||
</template>
|
class="funkwhale link artist"
|
||||||
</div>
|
>
|
||||||
|
<span
|
||||||
|
v-for="ac in object.track.artist_credit"
|
||||||
|
:key="ac.artist.id"
|
||||||
|
>
|
||||||
|
<router-link
|
||||||
|
class="discrete link"
|
||||||
|
:to="{ name: 'library.artists.detail', params: { id: ac.artist.id } }"
|
||||||
|
>
|
||||||
|
{{ ac.credit }}
|
||||||
|
</router-link>
|
||||||
|
<span v-if="ac.joinphrase">{{ ac.joinphrase }}</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<TagsList
|
||||||
|
label-classes="tiny"
|
||||||
|
:truncate-size="20"
|
||||||
|
:limit="2"
|
||||||
|
:show-more="false"
|
||||||
|
:tags="object.track.tags"
|
||||||
|
/>
|
||||||
|
<Spacer :size="4"/>
|
||||||
|
<div
|
||||||
|
v-if="isActivity"
|
||||||
|
class="extra"
|
||||||
|
>
|
||||||
|
<router-link
|
||||||
|
class="funkwhale link user"
|
||||||
|
:to="{name: 'profile.overview', params: {username: object.actor.name}}"
|
||||||
|
>
|
||||||
|
<span class="at symbol" />{{ object.actor.name }}
|
||||||
|
</router-link>
|
||||||
|
<span class="right floated"><human-date :date="object.creation_date" /></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<play-button
|
||||||
|
:account="object.actor"
|
||||||
|
:dropdown-only="true"
|
||||||
|
:track="object.track"
|
||||||
|
square-small
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Pagination
|
||||||
|
v-if="count > props.limit"
|
||||||
|
v-model:page="page"
|
||||||
|
:pages="Math.ceil((count || 0) / props.limit)"
|
||||||
|
style="grid-column: 1 / -1;"
|
||||||
|
/>
|
||||||
|
</Section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -9,9 +9,8 @@ import axios from 'axios'
|
||||||
|
|
||||||
import LibraryCard from '~/views/content/remote/Card.vue'
|
import LibraryCard from '~/views/content/remote/Card.vue'
|
||||||
import Button from '~/components/ui/Button.vue'
|
import Button from '~/components/ui/Button.vue'
|
||||||
import Layout from '~/components/ui/Layout.vue'
|
import Section from '~/components/ui/Section.vue'
|
||||||
import Loader from '~/components/ui/Loader.vue'
|
import Loader from '~/components/ui/Loader.vue'
|
||||||
import Spacer from '~/components/ui/Spacer.vue'
|
|
||||||
|
|
||||||
import useErrorHandler from '~/composables/useErrorHandler'
|
import useErrorHandler from '~/composables/useErrorHandler'
|
||||||
|
|
||||||
|
@ -21,6 +20,7 @@ interface Events {
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
url: string
|
url: string
|
||||||
|
title?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
|
@ -53,7 +53,7 @@ const fetchData = async (url = props.url) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
fetchData()
|
setTimeout(fetchData, 1000)
|
||||||
})
|
})
|
||||||
|
|
||||||
watch(() => props.url, () => {
|
watch(() => props.url, () => {
|
||||||
|
@ -62,37 +62,34 @@ watch(() => props.url, () => {
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="wrapper">
|
<Section
|
||||||
<h3
|
:h2="title"
|
||||||
v-if="!!$slots.title"
|
align-left
|
||||||
class="ui header"
|
small-items
|
||||||
>
|
>
|
||||||
<slot name="title" />
|
<Loader v-if="isLoading" style="grid-column: 1 / -1;" />
|
||||||
</h3>
|
|
||||||
<p
|
<p
|
||||||
v-if="!isLoading && libraries.length > 0"
|
v-if="!isLoading && libraries.length > 0"
|
||||||
|
style="grid-column: 1 / -1;"
|
||||||
class="ui subtitle"
|
class="ui subtitle"
|
||||||
>
|
>
|
||||||
<slot />
|
<slot />
|
||||||
</p>
|
</p>
|
||||||
<p
|
<p
|
||||||
v-if="!isLoading && libraries.length === 0"
|
v-if="!isLoading && libraries.length === 0"
|
||||||
|
style="grid-column: 1 / -1;"
|
||||||
class="ui subtitle"
|
class="ui subtitle"
|
||||||
>
|
>
|
||||||
{{ t('components.federation.LibraryWidget.empty.noMatch') }}
|
{{ t('components.federation.LibraryWidget.empty.noMatch') }}
|
||||||
</p>
|
</p>
|
||||||
<div class="ui hidden divider" />
|
<library-card
|
||||||
<Layout flex>
|
v-for="library in libraries"
|
||||||
<Loader v-if="isLoading" />
|
:key="library.uuid"
|
||||||
<library-card
|
:display-scan="false"
|
||||||
v-for="library in libraries"
|
:display-follow="store.state.auth.authenticated && library.actor.full_username != store.state.auth.fullUsername"
|
||||||
:key="library.uuid"
|
:initial-library="library"
|
||||||
:display-scan="false"
|
:display-copy-fid="true"
|
||||||
:display-follow="store.state.auth.authenticated && library.actor.full_username != store.state.auth.fullUsername"
|
/>
|
||||||
:initial-library="library"
|
|
||||||
:display-copy-fid="true"
|
|
||||||
/>
|
|
||||||
</Layout>
|
|
||||||
<template v-if="nextPage">
|
<template v-if="nextPage">
|
||||||
<Spacer />
|
<Spacer />
|
||||||
<Button
|
<Button
|
||||||
|
@ -103,5 +100,5 @@ watch(() => props.url, () => {
|
||||||
{{ t('components.federation.LibraryWidget.button.showMore') }}
|
{{ t('components.federation.LibraryWidget.button.showMore') }}
|
||||||
</Button>
|
</Button>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</Section>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -9,6 +9,9 @@ import Pagination from '~/components/vui/Pagination.vue'
|
||||||
import { computed, ref } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
|
|
||||||
|
import Loader from '~/components/ui/Loader.vue'
|
||||||
|
import Spacer from '~/components/ui/Spacer.vue'
|
||||||
|
|
||||||
interface Events {
|
interface Events {
|
||||||
(e: 'libraries-loaded', libraries: Library[]): void
|
(e: 'libraries-loaded', libraries: Library[]): void
|
||||||
}
|
}
|
||||||
|
@ -57,14 +60,14 @@ const paginatedDiscs = computed(() => props.object.tracks.slice(props.paginateBy
|
||||||
<div
|
<div
|
||||||
v-if="!isLoadingTracks"
|
v-if="!isLoadingTracks"
|
||||||
>
|
>
|
||||||
<!-- <h2 class="ui header">
|
<h2 class="ui header">
|
||||||
<span v-if="isSerie">
|
<span v-if="isSerie">
|
||||||
{{ t('components.library.AlbumDetail.header.episodes') }}
|
{{ t('components.library.AlbumDetail.header.episodes') }}
|
||||||
</span>
|
</span>
|
||||||
<span v-else>
|
<span v-else>
|
||||||
{{ t('components.library.AlbumDetail.header.tracks') }}
|
{{ t('components.library.AlbumDetail.header.tracks') }}
|
||||||
</span>
|
</span>
|
||||||
</h2> -->
|
</h2>
|
||||||
|
|
||||||
<channel-entries
|
<channel-entries
|
||||||
v-if="artistCredit && artistCredit[0].artist.channel && isSerie"
|
v-if="artistCredit && artistCredit[0].artist.channel && isSerie"
|
||||||
|
@ -73,6 +76,8 @@ const paginatedDiscs = computed(() => props.object.tracks.slice(props.paginateBy
|
||||||
:filters="{channel: artistCredit[0].artist.channel.uuid, album: object.id, ordering: '-creation_date'}"
|
:filters="{channel: artistCredit[0].artist.channel.uuid, album: object.id, ordering: '-creation_date'}"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<Loader v-if="isLoadingTracks" />
|
||||||
|
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<template v-if="discCount > 1">
|
<template v-if="discCount > 1">
|
||||||
<div
|
<div
|
||||||
|
@ -125,12 +130,11 @@ const paginatedDiscs = computed(() => props.object.tracks.slice(props.paginateBy
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template v-if="artistCredit && !artistCredit[0]?.artist.channel && !isSerie">
|
<template v-if="artistCredit && !artistCredit[0]?.artist.channel && !isSerie">
|
||||||
<h2>
|
<Spacer />
|
||||||
{{ t('components.library.AlbumDetail.header.libraries') }}
|
|
||||||
</h2>
|
|
||||||
<library-widget
|
<library-widget
|
||||||
:url="'albums/' + object.id + '/libraries/'"
|
:url="'albums/' + object.id + '/libraries/'"
|
||||||
@loaded="emit('libraries-loaded', $event)"
|
@loaded="emit('libraries-loaded', $event)"
|
||||||
|
:title="t('components.library.AlbumDetail.header.libraries')"
|
||||||
>
|
>
|
||||||
{{ t('components.library.AlbumDetail.description.libraries') }}
|
{{ t('components.library.AlbumDetail.description.libraries') }}
|
||||||
</library-widget>
|
</library-widget>
|
||||||
|
|
|
@ -80,29 +80,22 @@ fetchData()
|
||||||
v-if="scope === 'all'"
|
v-if="scope === 'all'"
|
||||||
:show-modification-date="true"
|
:show-modification-date="true"
|
||||||
:filters="{ordering: '-creation_date'}"
|
:filters="{ordering: '-creation_date'}"
|
||||||
:limit="12"
|
:limit="8"
|
||||||
:title="t('components.library.Home.header.newChannels')"
|
:title="t('components.library.Home.header.newChannels')"
|
||||||
>
|
/>
|
||||||
</channels-widget>
|
|
||||||
|
|
||||||
<track-widget
|
<track-widget
|
||||||
|
:title="t('components.library.Home.header.recentlyListened')"
|
||||||
:url="'history/listenings/'"
|
:url="'history/listenings/'"
|
||||||
:filters="{ scope, ordering: '-creation_date', ...qualityFilters }"
|
:filters="{ scope, ordering: '-creation_date', ...qualityFilters }"
|
||||||
:websocket-handlers="['Listen']"
|
:websocket-handlers="['Listen']"
|
||||||
>
|
/>
|
||||||
<template #title>
|
|
||||||
{{ t('components.library.Home.header.recentlyListened') }}
|
|
||||||
</template>
|
|
||||||
</track-widget>
|
|
||||||
|
|
||||||
<track-widget
|
<track-widget
|
||||||
|
:title="t('components.library.Home.header.recentlyFavorited')"
|
||||||
:url="'favorites/tracks/'"
|
:url="'favorites/tracks/'"
|
||||||
:filters="{scope: scope, ordering: '-creation_date'}"
|
:filters="{scope: scope, ordering: '-creation_date'}"
|
||||||
>
|
/>
|
||||||
<template #title>
|
|
||||||
{{ t('components.library.Home.header.recentlyFavorited') }}
|
|
||||||
</template>
|
|
||||||
</track-widget>
|
|
||||||
|
|
||||||
<album-widget :filters="{scope: scope, playable: true, ordering: '-creation_date', ...qualityFilters}">
|
<album-widget :filters="{scope: scope, playable: true, ordering: '-creation_date', ...qualityFilters}">
|
||||||
<template #title>
|
<template #title>
|
||||||
|
|
|
@ -9,17 +9,20 @@ import { useI18n } from 'vue-i18n'
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
|
|
||||||
import useErrorHandler from '~/composables/useErrorHandler'
|
import useErrorHandler from '~/composables/useErrorHandler'
|
||||||
|
import usePage from '~/composables/navigation/usePage'
|
||||||
|
|
||||||
import PlaylistCard from '~/components/playlists/Card.vue'
|
import PlaylistCard from '~/components/playlists/Card.vue'
|
||||||
import Button from '~/components/ui/Button.vue'
|
import Button from '~/components/ui/Button.vue'
|
||||||
import Layout from '~/components/ui/Layout.vue'
|
import Section from '~/components/ui/Section.vue'
|
||||||
import Alert from '~/components/ui/Alert.vue'
|
import Alert from '~/components/ui/Alert.vue'
|
||||||
import Spacer from '~/components/ui/Spacer.vue'
|
import Spacer from '~/components/ui/Spacer.vue'
|
||||||
import Loader from '~/components/ui/Loader.vue'
|
import Loader from '~/components/ui/Loader.vue'
|
||||||
|
import Pagination from '~/components/ui/Pagination.vue'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
filters: Record<string, unknown>
|
filters: Record<string, unknown>
|
||||||
url: string
|
url: string
|
||||||
|
title?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
|
@ -29,19 +32,25 @@ const props = defineProps<Props>()
|
||||||
const store = useStore()
|
const store = useStore()
|
||||||
|
|
||||||
const objects = reactive([] as Playlist[])
|
const objects = reactive([] as Playlist[])
|
||||||
const isLoading = ref(false)
|
const page = usePage()
|
||||||
const nextPage = ref('')
|
const nextPage = ref('')
|
||||||
|
const count = ref(0)
|
||||||
|
|
||||||
|
const isLoading = ref(false)
|
||||||
|
|
||||||
const fetchData = async (url = props.url) => {
|
const fetchData = async (url = props.url) => {
|
||||||
isLoading.value = true
|
isLoading.value = true
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const params = {
|
const params = {
|
||||||
...props.filters,
|
...props.filters,
|
||||||
page_size: props.filters.limit ?? 3
|
page: page.value,
|
||||||
|
page_size: props.filters.limit ?? 4
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await axios.get(url, { params })
|
const response = await axios.get(url, { params })
|
||||||
nextPage.value = response.data.next
|
nextPage.value = response.data.next
|
||||||
|
count.value = response.data.count
|
||||||
objects.push(...response.data.results)
|
objects.push(...response.data.results)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
useErrorHandler(error as Error)
|
useErrorHandler(error as Error)
|
||||||
|
@ -50,35 +59,24 @@ const fetchData = async (url = props.url) => {
|
||||||
isLoading.value = false
|
isLoading.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setTimeout(fetchData, 1000)
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => store.state.moderation.lastUpdate,
|
[() => store.state.moderation.lastUpdate, page],
|
||||||
() => fetchData(),
|
() => fetchData(),
|
||||||
{ immediate: true }
|
{ immediate: true }
|
||||||
)
|
)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="playlist-widget">
|
<Section
|
||||||
<h2 v-if="!!$slots.title">
|
align-left
|
||||||
<slot name="title" />
|
:h2="title"
|
||||||
</h2>
|
>
|
||||||
|
|
||||||
<Loader v-if="isLoading"/>
|
<Loader v-if="isLoading"/>
|
||||||
|
|
||||||
<Layout
|
|
||||||
v-else-if="objects.length > 0"
|
|
||||||
flex
|
|
||||||
gap-16
|
|
||||||
>
|
|
||||||
<PlaylistCard
|
|
||||||
v-for="playlist in objects"
|
|
||||||
:key="playlist.id"
|
|
||||||
:playlist="playlist"
|
|
||||||
/>
|
|
||||||
</Layout>
|
|
||||||
|
|
||||||
<Alert
|
<Alert
|
||||||
v-else
|
v-if="!isLoading && objects.length === 0"
|
||||||
|
style="grid-column: 1 / -1;"
|
||||||
blue
|
blue
|
||||||
align-items="center"
|
align-items="center"
|
||||||
>
|
>
|
||||||
|
@ -97,16 +95,15 @@ watch(
|
||||||
{{ t('components.playlists.Widget.button.create') }}
|
{{ t('components.playlists.Widget.button.create') }}
|
||||||
</Button>
|
</Button>
|
||||||
</Alert>
|
</Alert>
|
||||||
|
<PlaylistCard
|
||||||
<template v-if="nextPage">
|
v-for="playlist in objects"
|
||||||
<Spacer />
|
:key="playlist.id"
|
||||||
<Button
|
:playlist="playlist"
|
||||||
v-if="nextPage"
|
/>
|
||||||
primary
|
</Section>
|
||||||
@click="fetchData(nextPage)"
|
<Pagination
|
||||||
>
|
v-if="objects && count > props.filters.limit"
|
||||||
{{ t('components.playlists.Widget.button.more') }}
|
v-model:page="page"
|
||||||
</Button>
|
:pages="Math.ceil((count || 0) / props.filters.limit)"
|
||||||
</template>
|
/>
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -10,6 +10,7 @@ import TrackWidget from '~/components/audio/track/Widget.vue'
|
||||||
import AlbumWidget from '~/components/album/Widget.vue'
|
import AlbumWidget from '~/components/album/Widget.vue'
|
||||||
import RadioButton from '~/components/radios/Button.vue'
|
import RadioButton from '~/components/radios/Button.vue'
|
||||||
import Layout from '~/components/ui/Layout.vue'
|
import Layout from '~/components/ui/Layout.vue'
|
||||||
|
import Header from '~/components/ui/Header.vue'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
object?: Actor
|
object?: Actor
|
||||||
|
@ -26,47 +27,45 @@ const { t } = useI18n()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Layout stack>
|
<Layout stack gap-64>
|
||||||
<radio-button
|
<Header
|
||||||
v-if="recentActivity > 0"
|
align-left
|
||||||
class="right floated"
|
medium-items
|
||||||
type="account"
|
:h1="t('views.auth.ProfileBase.link.overview')"
|
||||||
:object?-id="{username: object?.preferred_username, fullUsername: object?.full_username}"
|
>
|
||||||
:client-only="true"
|
<template #action>
|
||||||
/>
|
<radio-button
|
||||||
|
v-if="recentActivity > 0"
|
||||||
|
class="right floated"
|
||||||
|
type="account"
|
||||||
|
:object-id="{username: object?.preferred_username, fullUsername: object?.full_username}"
|
||||||
|
:client-only="true"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</Header>
|
||||||
|
|
||||||
<track-widget
|
<track-widget
|
||||||
:url="'history/listenings/'"
|
:url="'history/listenings/'"
|
||||||
:filters="{ scope: `actor:${object?.full_username}`, ordering: '-creation_date', ...qualityFilters}"
|
:filters="{ scope: `actor:${object?.full_username}`, ordering: '-creation_date', ...qualityFilters}"
|
||||||
:websocket-handlers="['Listen']"
|
:websocket-handlers="['Listen']"
|
||||||
@count="recentActivity = $event"
|
@count="recentActivity = $event"
|
||||||
>
|
:title="t('components.library.Home.header.recentlyListened')"
|
||||||
<template #title>
|
/>
|
||||||
{{ t('components.library.Home.header.recentlyListened') }}
|
|
||||||
</template>
|
|
||||||
</track-widget>
|
|
||||||
|
|
||||||
<track-widget
|
<track-widget
|
||||||
:url="'favorites/tracks/'"
|
:url="'favorites/tracks/'"
|
||||||
:filters="{scope: 'actor:${object?.full_username}', ordering: '-creation_date'}"
|
:filters="{scope: `actor:${object?.full_username}`, ordering: '-creation_date'}"
|
||||||
>
|
:title="t('components.library.Home.header.recentlyFavorited')"
|
||||||
<template #title>
|
/>
|
||||||
{{ t('components.library.Home.header.recentlyFavorited') }}
|
|
||||||
</template>
|
|
||||||
</track-widget>
|
|
||||||
|
|
||||||
<playlist-widget
|
<playlist-widget
|
||||||
:url="'playlists/'"
|
:url="'playlists/'"
|
||||||
:filters="{scope: `actor:${object?.full_username}`, playable: true, ordering: '-modification_date'}"
|
:filters="{scope: `actor:${object?.full_username}`, playable: true, ordering: '-modification_date'}"
|
||||||
>
|
:title="t('views.auth.ProfileActivity.header.playlists')"
|
||||||
<template #title>
|
/>
|
||||||
{{ t('views.auth.ProfileActivity.header.playlists') }}
|
|
||||||
</template>
|
|
||||||
</playlist-widget>
|
|
||||||
|
|
||||||
<album-widget :filters="{scope: `actor:${object?.full_username}`, playable: true, ordering: '-creation_date', ...qualityFilters}">
|
<album-widget
|
||||||
<template #title>
|
:filters="{scope: `actor:${object?.full_username}`, playable: true, ordering: '-creation_date', ...qualityFilters}"
|
||||||
{{ t('components.library.Home.header.recentlyAdded') }}
|
:title="t('components.library.Home.header.recentlyAdded')"
|
||||||
</template>
|
/>
|
||||||
</album-widget>
|
|
||||||
</Layout>
|
</Layout>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -129,7 +129,6 @@ const recentActivity = ref(0)
|
||||||
</Layout>
|
</Layout>
|
||||||
<Tabs>
|
<Tabs>
|
||||||
<Tab :title="t('views.auth.ProfileBase.link.overview')" :to="{name: 'profile.overview', params: routerParams}">
|
<Tab :title="t('views.auth.ProfileBase.link.overview')" :to="{name: 'profile.overview', params: routerParams}">
|
||||||
<h2>{{ t('views.auth.ProfileBase.link.overview') }}</h2>
|
|
||||||
<router-view
|
<router-view
|
||||||
:object="object"
|
:object="object"
|
||||||
@updated="fetchData"
|
@updated="fetchData"
|
||||||
|
|
Loading…
Reference in New Issue