Migrate some components
This commit is contained in:
parent
b3a08c8688
commit
5b95a8f954
|
@ -54,6 +54,7 @@
|
||||||
"yarn": "^1.22.19"
|
"yarn": "^1.22.19"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/diff": "^5.0.2",
|
||||||
"@types/dompurify": "2.3.3",
|
"@types/dompurify": "2.3.3",
|
||||||
"@types/howler": "2.2.7",
|
"@types/howler": "2.2.7",
|
||||||
"@types/jest": "28.1.6",
|
"@types/jest": "28.1.6",
|
||||||
|
|
|
@ -1,3 +1,32 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { Track } from '~/types'
|
||||||
|
|
||||||
|
import { useGettext } from 'vue3-gettext'
|
||||||
|
import { useStore } from '~/store'
|
||||||
|
import { computed } from 'vue'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
track?: Track
|
||||||
|
button?: boolean
|
||||||
|
border?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
track: () => ({} as Track),
|
||||||
|
button: false,
|
||||||
|
border: false
|
||||||
|
})
|
||||||
|
|
||||||
|
const { $pgettext } = useGettext()
|
||||||
|
const store = useStore()
|
||||||
|
|
||||||
|
const isFavorite = computed(() => store.getters['favorites/isFavorite'](props.track.id))
|
||||||
|
const title = computed(() => isFavorite.value
|
||||||
|
? $pgettext('Content/Track/Icon.Tooltip/Verb', 'Remove from favorites')
|
||||||
|
: $pgettext('Content/Track/*/Verb', 'Add to favorites')
|
||||||
|
)
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<button
|
<button
|
||||||
v-if="button"
|
v-if="button"
|
||||||
|
@ -28,26 +57,3 @@
|
||||||
<i :class="['heart', {'pink': isFavorite}, 'basic', 'icon']" />
|
<i :class="['heart', {'pink': isFavorite}, 'basic', 'icon']" />
|
||||||
</button>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
|
||||||
export default {
|
|
||||||
props: {
|
|
||||||
track: { type: Object, default: () => { return {} } },
|
|
||||||
button: { type: Boolean, default: false },
|
|
||||||
border: { type: Boolean, default: false }
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
title () {
|
|
||||||
if (this.isFavorite) {
|
|
||||||
return this.$pgettext('Content/Track/Icon.Tooltip/Verb', 'Remove from favorites')
|
|
||||||
} else {
|
|
||||||
return this.$pgettext('Content/Track/*/Verb', 'Add to favorites')
|
|
||||||
}
|
|
||||||
},
|
|
||||||
isFavorite () {
|
|
||||||
return this.$store.getters['favorites/isFavorite'](this.track.id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
|
@ -1,3 +1,48 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { Library } from '~/types'
|
||||||
|
|
||||||
|
import { ref, reactive } from 'vue'
|
||||||
|
|
||||||
|
import axios from 'axios'
|
||||||
|
import LibraryCard from '~/views/content/remote/Card.vue'
|
||||||
|
|
||||||
|
interface Emits {
|
||||||
|
(e: 'loaded', libraries: Library[]): void
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
url: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const emit = defineEmits<Emits>()
|
||||||
|
const props = defineProps<Props>()
|
||||||
|
|
||||||
|
const nextPage = ref()
|
||||||
|
const libraries = reactive([] as Library[])
|
||||||
|
const isLoading = ref(false)
|
||||||
|
const fetchData = async (url = props.url) => {
|
||||||
|
isLoading.value = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await axios.get(url, {
|
||||||
|
params: {
|
||||||
|
page_size: 6
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
nextPage.value = response.data.next
|
||||||
|
libraries.push(...response.data.results)
|
||||||
|
emit('loaded', libraries)
|
||||||
|
} catch (error) {
|
||||||
|
// TODO (wvffle): Handle error
|
||||||
|
}
|
||||||
|
|
||||||
|
isLoading.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchData()
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="wrapper">
|
<div class="wrapper">
|
||||||
<h3
|
<h3
|
||||||
|
@ -51,62 +96,3 @@
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
|
||||||
import { clone } from 'lodash-es'
|
|
||||||
import axios from 'axios'
|
|
||||||
import LibraryCard from '~/views/content/remote/Card.vue'
|
|
||||||
|
|
||||||
export default {
|
|
||||||
components: {
|
|
||||||
LibraryCard
|
|
||||||
},
|
|
||||||
props: {
|
|
||||||
url: { type: String, required: true }
|
|
||||||
},
|
|
||||||
data () {
|
|
||||||
return {
|
|
||||||
libraries: [],
|
|
||||||
limit: 6,
|
|
||||||
isLoading: false,
|
|
||||||
errors: null,
|
|
||||||
previousPage: null,
|
|
||||||
nextPage: null
|
|
||||||
}
|
|
||||||
},
|
|
||||||
watch: {
|
|
||||||
offset () {
|
|
||||||
this.fetchData()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
created () {
|
|
||||||
this.fetchData(this.url)
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
fetchData (url) {
|
|
||||||
this.isLoading = true
|
|
||||||
const self = this
|
|
||||||
const params = clone({})
|
|
||||||
params.page_size = this.limit
|
|
||||||
params.offset = this.offset
|
|
||||||
axios.get(url, { params }).then((response) => {
|
|
||||||
self.previousPage = response.data.previous
|
|
||||||
self.nextPage = response.data.next
|
|
||||||
self.isLoading = false
|
|
||||||
self.libraries = [...self.libraries, ...response.data.results]
|
|
||||||
self.$emit('loaded', self.libraries)
|
|
||||||
}, error => {
|
|
||||||
self.isLoading = false
|
|
||||||
self.errors = error.backendErrors
|
|
||||||
})
|
|
||||||
},
|
|
||||||
updateOffset (increment) {
|
|
||||||
if (increment) {
|
|
||||||
this.offset += this.limit
|
|
||||||
} else {
|
|
||||||
this.offset = Math.max(this.offset - this.limit, 0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
|
@ -200,7 +200,10 @@ const remove = async () => {
|
||||||
>
|
>
|
||||||
{{ object.title }}
|
{{ object.title }}
|
||||||
</h2>
|
</h2>
|
||||||
<artist-label :artist="artist" />
|
<artist-label
|
||||||
|
v-if="artist"
|
||||||
|
:artist="artist"
|
||||||
|
/>
|
||||||
</header>
|
</header>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
|
@ -228,8 +231,9 @@ const remove = async () => {
|
||||||
{{ object.title }}
|
{{ object.title }}
|
||||||
</h2>
|
</h2>
|
||||||
<artist-label
|
<artist-label
|
||||||
class="rounded"
|
v-if="artist"
|
||||||
:artist="artist"
|
:artist="artist"
|
||||||
|
class="rounded"
|
||||||
/>
|
/>
|
||||||
</header>
|
</header>
|
||||||
<div
|
<div
|
||||||
|
|
|
@ -1,3 +1,34 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { Artist, Album, Library, Track } from '~/types'
|
||||||
|
|
||||||
|
import LibraryWidget from '~/components/federation/LibraryWidget.vue'
|
||||||
|
import ChannelEntries from '~/components/audio/ChannelEntries.vue'
|
||||||
|
import TrackTable from '~/components/audio/track/Table.vue'
|
||||||
|
import PlayButton from '~/components/audio/PlayButton.vue'
|
||||||
|
|
||||||
|
interface Emits {
|
||||||
|
(e: 'page-changed', page: number): void
|
||||||
|
(e: 'libraries-loaded', libraries: Library[]): void
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
object: Album
|
||||||
|
|
||||||
|
discs: Track[][]
|
||||||
|
|
||||||
|
isSerie: boolean
|
||||||
|
artist: Artist
|
||||||
|
page: number
|
||||||
|
paginateBy: number
|
||||||
|
totalTracks: number
|
||||||
|
}
|
||||||
|
|
||||||
|
const emit = defineEmits<Emits>()
|
||||||
|
defineProps<Props>()
|
||||||
|
|
||||||
|
const getDiscKey = (disc: Track[]) => disc.map(track => track.id).join('|')
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div v-if="object">
|
<div v-if="object">
|
||||||
<h2 class="ui header">
|
<h2 class="ui header">
|
||||||
|
@ -20,10 +51,10 @@
|
||||||
:limit="50"
|
:limit="50"
|
||||||
:filters="{channel: artist.channel.uuid, album: object.id, ordering: '-creation_date'}"
|
:filters="{channel: artist.channel.uuid, album: object.id, ordering: '-creation_date'}"
|
||||||
/>
|
/>
|
||||||
<template v-else-if="discs && discs.length > 1">
|
<template v-else-if="discs.length > 1">
|
||||||
<div
|
<div
|
||||||
v-for="tracks in discs"
|
v-for="tracks in discs"
|
||||||
:key="tracks.disc_number"
|
:key="getDiscKey(tracks)"
|
||||||
>
|
>
|
||||||
<div class="ui hidden divider" />
|
<div class="ui hidden divider" />
|
||||||
<play-button
|
<play-button
|
||||||
|
@ -48,7 +79,7 @@
|
||||||
:total="totalTracks"
|
:total="totalTracks"
|
||||||
:paginate-by="paginateBy"
|
:paginate-by="paginateBy"
|
||||||
:page="page"
|
:page="page"
|
||||||
@page-changed="updatePage"
|
@page-changed="emit('page-changed', page)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
@ -64,7 +95,7 @@
|
||||||
:total="totalTracks"
|
:total="totalTracks"
|
||||||
:paginate-by="paginateBy"
|
:paginate-by="paginateBy"
|
||||||
:page="page"
|
:page="page"
|
||||||
@page-changed="updatePage"
|
@page-changed="emit('page-changed', page)"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
<template v-if="!artist.channel && !isSerie">
|
<template v-if="!artist.channel && !isSerie">
|
||||||
|
@ -75,7 +106,7 @@
|
||||||
</h2>
|
</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)"
|
||||||
>
|
>
|
||||||
<translate translate-context="Content/Album/Paragraph">
|
<translate translate-context="Content/Album/Paragraph">
|
||||||
This album is present in the following libraries:
|
This album is present in the following libraries:
|
||||||
|
@ -84,41 +115,3 @@
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
|
||||||
|
|
||||||
import time from '~/utils/time'
|
|
||||||
import LibraryWidget from '~/components/federation/LibraryWidget.vue'
|
|
||||||
import ChannelEntries from '~/components/audio/ChannelEntries.vue'
|
|
||||||
import TrackTable from '~/components/audio/track/Table.vue'
|
|
||||||
import PlayButton from '~/components/audio/PlayButton.vue'
|
|
||||||
|
|
||||||
export default {
|
|
||||||
components: {
|
|
||||||
LibraryWidget,
|
|
||||||
TrackTable,
|
|
||||||
ChannelEntries,
|
|
||||||
PlayButton
|
|
||||||
},
|
|
||||||
props: {
|
|
||||||
object: { type: Object, required: true },
|
|
||||||
discs: { type: Array, required: true },
|
|
||||||
isSerie: { type: Boolean, required: true },
|
|
||||||
artist: { type: Object, required: true },
|
|
||||||
page: { type: Number, required: true },
|
|
||||||
paginateBy: { type: Number, required: true },
|
|
||||||
totalTracks: { type: Number, required: true }
|
|
||||||
},
|
|
||||||
data () {
|
|
||||||
return {
|
|
||||||
time,
|
|
||||||
id: this.object.id
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
updatePage: function (page) {
|
|
||||||
this.$emit('page-changed', page)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
|
@ -1,3 +1,22 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { Album, Library } from '~/types'
|
||||||
|
|
||||||
|
import { useStore } from '~/store'
|
||||||
|
|
||||||
|
import EditForm from '~/components/library/EditForm.vue'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
objectType: string
|
||||||
|
object: Album
|
||||||
|
libraries: Library[]
|
||||||
|
}
|
||||||
|
|
||||||
|
defineProps<Props>()
|
||||||
|
|
||||||
|
const store = useStore()
|
||||||
|
const canEdit = store.state.auth.availablePermissions.library
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<section class="ui vertical stripe segment">
|
<section class="ui vertical stripe segment">
|
||||||
<div class="ui text container">
|
<div class="ui text container">
|
||||||
|
@ -32,27 +51,3 @@
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
|
||||||
import EditForm from '~/components/library/EditForm.vue'
|
|
||||||
export default {
|
|
||||||
components: {
|
|
||||||
EditForm
|
|
||||||
},
|
|
||||||
props: {
|
|
||||||
objectType: { type: String, required: true },
|
|
||||||
object: { type: Object, required: true },
|
|
||||||
libraries: { type: Array, required: true }
|
|
||||||
},
|
|
||||||
data () {
|
|
||||||
return {
|
|
||||||
id: this.object.id
|
|
||||||
}
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
canEdit () {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
|
@ -1,3 +1,54 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { Artist, Track, Album } from '~/types'
|
||||||
|
import type { ContentFilter } from '~/store/moderation'
|
||||||
|
|
||||||
|
import { ref, computed, reactive } from 'vue'
|
||||||
|
import { useStore } from '~/store'
|
||||||
|
|
||||||
|
import axios from 'axios'
|
||||||
|
|
||||||
|
import AlbumCard from '~/components/audio/album/Card.vue'
|
||||||
|
import TrackTable from '~/components/audio/track/Table.vue'
|
||||||
|
import LibraryWidget from '~/components/federation/LibraryWidget.vue'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
object: Artist
|
||||||
|
tracks: Track[]
|
||||||
|
albums: Album[]
|
||||||
|
isLoadingAlbums: boolean
|
||||||
|
nextTracksUrl?: string | null
|
||||||
|
nextAlbumsUrl?: string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
nextTracksUrl: null,
|
||||||
|
nextAlbumsUrl: null
|
||||||
|
})
|
||||||
|
|
||||||
|
const store = useStore()
|
||||||
|
|
||||||
|
const additionalAlbums = reactive([] as Album[])
|
||||||
|
const contentFilter = computed(() => store.getters['moderation/artistFilters']().find((filter: ContentFilter) => filter.target.id === props.object.id))
|
||||||
|
const allAlbums = computed(() => [...props.albums, ...additionalAlbums])
|
||||||
|
|
||||||
|
const isLoadingMoreAlbums = ref(false)
|
||||||
|
const loadMoreAlbumsUrl = ref(props.nextAlbumsUrl)
|
||||||
|
const loadMoreAlbums = async () => {
|
||||||
|
if (loadMoreAlbumsUrl.value === null) return
|
||||||
|
isLoadingMoreAlbums.value = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await axios.get(loadMoreAlbumsUrl.value)
|
||||||
|
additionalAlbums.push(...additionalAlbums.concat(response.data.results))
|
||||||
|
loadMoreAlbumsUrl.value = response.data.next
|
||||||
|
} catch (error) {
|
||||||
|
// TODO (wvffle): Handle error
|
||||||
|
}
|
||||||
|
|
||||||
|
isLoadingMoreAlbums.value = false
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div v-if="object">
|
<div v-if="object">
|
||||||
<div
|
<div
|
||||||
|
@ -53,9 +104,9 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="ui hidden divider" />
|
<div class="ui hidden divider" />
|
||||||
<button
|
<button
|
||||||
v-if="nextAlbumsUrl && loadMoreAlbumsUrl"
|
v-if="loadMoreAlbumsUrl !== null"
|
||||||
:class="['ui', {loading: isLoadingMoreAlbums}, 'button']"
|
:class="['ui', {loading: isLoadingMoreAlbums}, 'button']"
|
||||||
@click="loadMoreAlbums(loadMoreAlbumsUrl)"
|
@click="loadMoreAlbums()"
|
||||||
>
|
>
|
||||||
<translate translate-context="Content/*/Button.Label">
|
<translate translate-context="Content/*/Button.Label">
|
||||||
Load more…
|
Load more…
|
||||||
|
@ -99,56 +150,3 @@
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
|
||||||
import axios from 'axios'
|
|
||||||
import AlbumCard from '~/components/audio/album/Card.vue'
|
|
||||||
import TrackTable from '~/components/audio/track/Table.vue'
|
|
||||||
import LibraryWidget from '~/components/federation/LibraryWidget.vue'
|
|
||||||
|
|
||||||
export default {
|
|
||||||
components: {
|
|
||||||
AlbumCard,
|
|
||||||
TrackTable,
|
|
||||||
LibraryWidget
|
|
||||||
},
|
|
||||||
props: {
|
|
||||||
object: { type: Object, required: true },
|
|
||||||
tracks: { type: Array, required: true },
|
|
||||||
albums: { type: Array, required: true },
|
|
||||||
isLoadingAlbums: { type: Boolean, required: true },
|
|
||||||
nextTracksUrl: { type: String, default: null },
|
|
||||||
nextAlbumsUrl: { type: String, default: null }
|
|
||||||
},
|
|
||||||
data () {
|
|
||||||
return {
|
|
||||||
loadMoreAlbumsUrl: this.nextAlbumsUrl,
|
|
||||||
additionalAlbums: [],
|
|
||||||
isLoadingMoreAlbums: false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
contentFilter () {
|
|
||||||
return this.$store.getters['moderation/artistFilters']().filter((e) => {
|
|
||||||
return e.target.id === this.object.id
|
|
||||||
})[0]
|
|
||||||
},
|
|
||||||
allAlbums () {
|
|
||||||
return this.albums.concat(this.additionalAlbums)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
loadMoreAlbums (url) {
|
|
||||||
const self = this
|
|
||||||
self.isLoadingMoreAlbums = true
|
|
||||||
axios.get(url).then((response) => {
|
|
||||||
self.additionalAlbums = self.additionalAlbums.concat(response.data.results)
|
|
||||||
self.loadMoreAlbumsUrl = response.data.next
|
|
||||||
self.isLoadingMoreAlbums = false
|
|
||||||
}, () => {
|
|
||||||
self.isLoadingMoreAlbums = false
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
|
@ -1,3 +1,22 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { Artist, Library } from '~/types'
|
||||||
|
|
||||||
|
import { useStore } from '~/store'
|
||||||
|
|
||||||
|
import EditForm from '~/components/library/EditForm.vue'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
objectType: string
|
||||||
|
object: Artist
|
||||||
|
libraries: Library[]
|
||||||
|
}
|
||||||
|
|
||||||
|
defineProps<Props>()
|
||||||
|
|
||||||
|
const store = useStore()
|
||||||
|
const canEdit = store.state.auth.availablePermissions.library
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<section class="ui vertical stripe segment">
|
<section class="ui vertical stripe segment">
|
||||||
<div class="ui text container">
|
<div class="ui text container">
|
||||||
|
@ -32,27 +51,3 @@
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
|
||||||
import EditForm from '~/components/library/EditForm.vue'
|
|
||||||
export default {
|
|
||||||
components: {
|
|
||||||
EditForm
|
|
||||||
},
|
|
||||||
props: {
|
|
||||||
objectType: { type: String, required: true },
|
|
||||||
object: { type: Object, required: true },
|
|
||||||
libraries: { type: Array, required: true }
|
|
||||||
},
|
|
||||||
data () {
|
|
||||||
return {
|
|
||||||
id: this.object.id
|
|
||||||
}
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
canEdit () {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
|
@ -1,3 +1,147 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { Review, ReviewState, ReviewStatePayload } from '~/types'
|
||||||
|
import type { Change } from 'diff'
|
||||||
|
|
||||||
|
import { diffWordsWithSpace } from 'diff'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import { computed, ref } from 'vue'
|
||||||
|
import { useStore } from '~/store'
|
||||||
|
|
||||||
|
import axios from 'axios'
|
||||||
|
|
||||||
|
import useEditConfigs from '~/composables/moderation/useEditConfigs'
|
||||||
|
|
||||||
|
interface Emits {
|
||||||
|
(e: 'approved', isApproved: boolean): void
|
||||||
|
(e: 'deleted'): void
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
obj: Review
|
||||||
|
currentState?: ReviewState
|
||||||
|
}
|
||||||
|
|
||||||
|
const emit = defineEmits<Emits>()
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
currentState: () => ({})
|
||||||
|
})
|
||||||
|
|
||||||
|
const configs = useEditConfigs()
|
||||||
|
const router = useRouter()
|
||||||
|
const store = useStore()
|
||||||
|
|
||||||
|
const canApprove = computed(() => props.obj.is_applied || store.state.auth.authenticated
|
||||||
|
? false
|
||||||
|
: store.state.auth.availablePermissions.library
|
||||||
|
)
|
||||||
|
|
||||||
|
const canDelete = computed(() => {
|
||||||
|
if (props.obj.is_applied || props.obj.is_approved) return false
|
||||||
|
if (!store.state.auth.authenticated) return false
|
||||||
|
|
||||||
|
// TODO (wvffle): Is it better to compare ids? Is full_username unique?
|
||||||
|
return props.obj.created_by.full_username === store.state.auth.fullUsername
|
||||||
|
|| store.state.auth.availablePermissions.library
|
||||||
|
})
|
||||||
|
|
||||||
|
const previousState = computed(() => props.obj.is_applied
|
||||||
|
// mutation was applied, we use the previous state that is stored
|
||||||
|
// on the mutation itself
|
||||||
|
? props.obj.previous_state
|
||||||
|
// mutation is not applied yet, so we use the current state that was
|
||||||
|
// passed to the component, if any
|
||||||
|
: props.currentState
|
||||||
|
)
|
||||||
|
|
||||||
|
const detailUrl = computed(() => {
|
||||||
|
if (!props.obj.target) return ''
|
||||||
|
|
||||||
|
const name = props.obj.target.type === 'track'
|
||||||
|
? 'library.tracks.edit.detail'
|
||||||
|
: props.obj.target.type === 'album'
|
||||||
|
? 'library.albums.edit.detail'
|
||||||
|
: props.obj.target.type === 'artist'
|
||||||
|
? 'library.artists.edit.detail'
|
||||||
|
: undefined
|
||||||
|
|
||||||
|
return router.resolve({
|
||||||
|
name,
|
||||||
|
params: {
|
||||||
|
id: props.obj.target.id,
|
||||||
|
editId: props.obj.uuid
|
||||||
|
}
|
||||||
|
}).href
|
||||||
|
})
|
||||||
|
|
||||||
|
const updatedFields = computed(() => {
|
||||||
|
if (!props.obj?.target) return []
|
||||||
|
|
||||||
|
const payload = props.obj.payload
|
||||||
|
const fields = Object.keys(payload)
|
||||||
|
|
||||||
|
const state = previousState.value
|
||||||
|
|
||||||
|
return fields.map((id) => {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||||
|
const config = configs[props.obj.target!.type].fields.find((field) => id === field.id)
|
||||||
|
const getValueRepr = config?.getValueRepr || (v => v)
|
||||||
|
|
||||||
|
const result = {
|
||||||
|
id,
|
||||||
|
config,
|
||||||
|
new: payload[id],
|
||||||
|
newRepr: getValueRepr(payload[id]) ?? '',
|
||||||
|
old: undefined as ReviewStatePayload,
|
||||||
|
oldRepr: '',
|
||||||
|
diff: [] as Change[]
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state?.[id]) {
|
||||||
|
const oldState = state[id]
|
||||||
|
result.old = oldState
|
||||||
|
result.oldRepr = getValueRepr(typeof oldState === 'string' ? oldState : oldState?.value) ?? ''
|
||||||
|
|
||||||
|
// we compute the diffs between the old and new values
|
||||||
|
result.diff = diffWordsWithSpace(result.oldRepr, result.newRepr)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
const isLoading = ref(false)
|
||||||
|
const remove = async () => {
|
||||||
|
isLoading.value = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
await axios.delete(`mutations/${props.obj.uuid}/`)
|
||||||
|
emit('deleted')
|
||||||
|
} catch (error) {
|
||||||
|
// TODO (wvffle): Handle error
|
||||||
|
}
|
||||||
|
|
||||||
|
isLoading.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
const approve = async (approved: boolean) => {
|
||||||
|
const url = approved
|
||||||
|
? `mutations/${props.obj.uuid}/approve/`
|
||||||
|
: `mutations/${props.obj.uuid}/reject/`
|
||||||
|
|
||||||
|
isLoading.value = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
await axios.post(url)
|
||||||
|
emit('approved', approved)
|
||||||
|
store.commit('ui/incrementNotifications', { count: -1, type: 'pendingReviewEdits' })
|
||||||
|
} catch (error) {
|
||||||
|
// TODO (wvffle): Handle error
|
||||||
|
}
|
||||||
|
|
||||||
|
isLoading.value = false
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="ui fluid card">
|
<div class="ui fluid card">
|
||||||
<div class="content">
|
<div class="content">
|
||||||
|
@ -88,7 +232,7 @@
|
||||||
<td>{{ field.id }}</td>
|
<td>{{ field.id }}</td>
|
||||||
|
|
||||||
<td v-if="field.diff">
|
<td v-if="field.diff">
|
||||||
<template v-if="field.config.type === 'attachment' && field.oldRepr">
|
<template v-if="field.config?.type === 'attachment' && field.oldRepr">
|
||||||
<img
|
<img
|
||||||
class="ui image"
|
class="ui image"
|
||||||
alt=""
|
alt=""
|
||||||
|
@ -115,7 +259,7 @@
|
||||||
v-if="field.diff"
|
v-if="field.diff"
|
||||||
:title="field.newRepr"
|
:title="field.newRepr"
|
||||||
>
|
>
|
||||||
<template v-if="field.config.type === 'attachment' && field.newRepr">
|
<template v-if="field.config?.type === 'attachment' && field.newRepr">
|
||||||
<img
|
<img
|
||||||
class="ui image"
|
class="ui image"
|
||||||
alt=""
|
alt=""
|
||||||
|
@ -136,7 +280,7 @@
|
||||||
v-else
|
v-else
|
||||||
:title="field.newRepr"
|
:title="field.newRepr"
|
||||||
>
|
>
|
||||||
<template v-if="field.config.type === 'attachment' && field.newRepr">
|
<template v-if="field.config?.type === 'attachment' && field.newRepr">
|
||||||
<img
|
<img
|
||||||
class="ui image"
|
class="ui image"
|
||||||
alt=""
|
alt=""
|
||||||
|
@ -214,135 +358,3 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
|
||||||
import axios from 'axios'
|
|
||||||
import { diffWordsWithSpace } from 'diff'
|
|
||||||
|
|
||||||
import useEditConfigs from '~/composables/moderation/useEditConfigs'
|
|
||||||
|
|
||||||
function castValue (value) {
|
|
||||||
if (value === null || value === undefined) {
|
|
||||||
return ''
|
|
||||||
}
|
|
||||||
return String(value)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default {
|
|
||||||
props: {
|
|
||||||
obj: { type: Object, required: true },
|
|
||||||
currentState: { type: Object, required: false, default: function () { return { } } }
|
|
||||||
},
|
|
||||||
setup () {
|
|
||||||
return { configs: useEditConfigs() }
|
|
||||||
},
|
|
||||||
data () {
|
|
||||||
return {
|
|
||||||
isLoading: false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
canApprove () {
|
|
||||||
if (this.obj.is_applied) return false
|
|
||||||
if (!this.$store.state.auth.authenticated) return false
|
|
||||||
return this.$store.state.auth.availablePermissions.library
|
|
||||||
},
|
|
||||||
canDelete () {
|
|
||||||
if (this.obj.is_applied || this.obj.is_approved) return false
|
|
||||||
if (!this.$store.state.auth.authenticated) return false
|
|
||||||
|
|
||||||
// TODO (wvffle): Is it better to compare ids? Is full_username unique?
|
|
||||||
return this.obj.created_by.full_username === this.$store.state.auth.fullUsername
|
|
||||||
|| this.$store.state.auth.availablePermissions.library
|
|
||||||
},
|
|
||||||
previousState () {
|
|
||||||
if (this.obj.is_applied) {
|
|
||||||
// mutation was applied, we use the previous state that is stored
|
|
||||||
// on the mutation itself
|
|
||||||
return this.obj.previous_state
|
|
||||||
}
|
|
||||||
// mutation is not applied yet, so we use the current state that was
|
|
||||||
// passed to the component, if any
|
|
||||||
return this.currentState
|
|
||||||
},
|
|
||||||
detailUrl () {
|
|
||||||
if (!this.obj.target) {
|
|
||||||
return ''
|
|
||||||
}
|
|
||||||
let namespace
|
|
||||||
const id = this.obj.target.id
|
|
||||||
if (this.obj.target.type === 'track') {
|
|
||||||
namespace = 'library.tracks.edit.detail'
|
|
||||||
}
|
|
||||||
if (this.obj.target.type === 'album') {
|
|
||||||
namespace = 'library.albums.edit.detail'
|
|
||||||
}
|
|
||||||
if (this.obj.target.type === 'artist') {
|
|
||||||
namespace = 'library.artists.edit.detail'
|
|
||||||
}
|
|
||||||
return this.$router.resolve({ name: namespace, params: { id, editId: this.obj.uuid } }).href
|
|
||||||
},
|
|
||||||
|
|
||||||
updatedFields () {
|
|
||||||
if (!this.obj || !this.obj.target) {
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
const payload = this.obj.payload
|
|
||||||
const previousState = this.previousState
|
|
||||||
const fields = Object.keys(payload)
|
|
||||||
const self = this
|
|
||||||
|
|
||||||
return fields.map((field) => {
|
|
||||||
const fieldConfig = this.configs[this.obj.target.type].fields.find(({ id }) => id === field)
|
|
||||||
const getValueRepr = fieldConfig.getValueRepr || (v => v)
|
|
||||||
const result = {
|
|
||||||
id: field,
|
|
||||||
config: fieldConfig
|
|
||||||
}
|
|
||||||
if (previousState?.[field]) {
|
|
||||||
result.old = previousState[field]
|
|
||||||
// TODO (wvffle): Originally it was using just result.old.value though for some reason it returns me the object as result.old now
|
|
||||||
result.oldRepr = castValue(getValueRepr(result.old.value ?? result.old))
|
|
||||||
}
|
|
||||||
result.new = payload[field]
|
|
||||||
result.newRepr = castValue(getValueRepr(result.new))
|
|
||||||
if (result.old) {
|
|
||||||
// we compute the diffs between the old and new values
|
|
||||||
result.diff = diffWordsWithSpace(result.oldRepr, result.newRepr)
|
|
||||||
}
|
|
||||||
|
|
||||||
return result
|
|
||||||
})
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
remove () {
|
|
||||||
const self = this
|
|
||||||
this.isLoading = true
|
|
||||||
axios.delete(`mutations/${this.obj.uuid}/`).then((response) => {
|
|
||||||
self.$emit('deleted')
|
|
||||||
self.isLoading = false
|
|
||||||
}, () => {
|
|
||||||
self.isLoading = false
|
|
||||||
})
|
|
||||||
},
|
|
||||||
approve (approved) {
|
|
||||||
let url
|
|
||||||
if (approved) {
|
|
||||||
url = `mutations/${this.obj.uuid}/approve/`
|
|
||||||
} else {
|
|
||||||
url = `mutations/${this.obj.uuid}/reject/`
|
|
||||||
}
|
|
||||||
const self = this
|
|
||||||
this.isLoading = true
|
|
||||||
axios.post(url).then((response) => {
|
|
||||||
self.$emit('approved', approved)
|
|
||||||
self.isLoading = false
|
|
||||||
self.$store.commit('ui/incrementNotifications', { count: -1, type: 'pendingReviewEdits' })
|
|
||||||
}, () => {
|
|
||||||
self.isLoading = false
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
|
@ -400,6 +400,27 @@ export interface ReportTarget {
|
||||||
type: EntityObjectType
|
type: EntityObjectType
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type ReviewStatePayload = { value: string } | string | undefined
|
||||||
|
export interface ReviewState {
|
||||||
|
[id: string]: ReviewStatePayload
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Review {
|
||||||
|
uuid: string
|
||||||
|
is_applied: boolean
|
||||||
|
is_approved: boolean
|
||||||
|
created_by: Actor
|
||||||
|
previous_state: ReviewState
|
||||||
|
payload: ReviewState
|
||||||
|
target?: ReportTarget & {
|
||||||
|
type: 'artist' | 'album' | 'track'
|
||||||
|
repr: string
|
||||||
|
}
|
||||||
|
creation_date: string
|
||||||
|
summary?: string
|
||||||
|
type: 'update'
|
||||||
|
}
|
||||||
|
|
||||||
export interface Report {
|
export interface Report {
|
||||||
uuid: string
|
uuid: string
|
||||||
summary?: string
|
summary?: string
|
||||||
|
|
|
@ -1452,6 +1452,11 @@
|
||||||
resolved "https://registry.yarnpkg.com/@types/cookie/-/cookie-0.3.3.tgz#85bc74ba782fb7aa3a514d11767832b0e3bc6803"
|
resolved "https://registry.yarnpkg.com/@types/cookie/-/cookie-0.3.3.tgz#85bc74ba782fb7aa3a514d11767832b0e3bc6803"
|
||||||
integrity sha512-LKVP3cgXBT9RYj+t+9FDKwS5tdI+rPBXaNSkma7hvqy35lc7mAokC2zsqWJH0LaqIt3B962nuYI77hsJoT1gow==
|
integrity sha512-LKVP3cgXBT9RYj+t+9FDKwS5tdI+rPBXaNSkma7hvqy35lc7mAokC2zsqWJH0LaqIt3B962nuYI77hsJoT1gow==
|
||||||
|
|
||||||
|
"@types/diff@^5.0.2":
|
||||||
|
version "5.0.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/diff/-/diff-5.0.2.tgz#dd565e0086ccf8bc6522c6ebafd8a3125c91c12b"
|
||||||
|
integrity sha512-uw8eYMIReOwstQ0QKF0sICefSy8cNO/v7gOTiIy9SbwuHyEecJUm7qlgueOO5S1udZ5I/irVydHVwMchgzbKTg==
|
||||||
|
|
||||||
"@types/dompurify@2.3.3":
|
"@types/dompurify@2.3.3":
|
||||||
version "2.3.3"
|
version "2.3.3"
|
||||||
resolved "https://registry.yarnpkg.com/@types/dompurify/-/dompurify-2.3.3.tgz#c24c92f698f77ed9cc9d9fa7888f90cf2bfaa23f"
|
resolved "https://registry.yarnpkg.com/@types/dompurify/-/dompurify-2.3.3.tgz#c24c92f698f77ed9cc9d9fa7888f90cf2bfaa23f"
|
||||||
|
|
Loading…
Reference in New Issue