Migrate some components
This commit is contained in:
parent
b3a08c8688
commit
5b95a8f954
|
@ -54,6 +54,7 @@
|
|||
"yarn": "^1.22.19"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/diff": "^5.0.2",
|
||||
"@types/dompurify": "2.3.3",
|
||||
"@types/howler": "2.2.7",
|
||||
"@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>
|
||||
<button
|
||||
v-if="button"
|
||||
|
@ -28,26 +57,3 @@
|
|||
<i :class="['heart', {'pink': isFavorite}, 'basic', 'icon']" />
|
||||
</button>
|
||||
</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>
|
||||
<div class="wrapper">
|
||||
<h3
|
||||
|
@ -51,62 +96,3 @@
|
|||
</template>
|
||||
</div>
|
||||
</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 }}
|
||||
</h2>
|
||||
<artist-label :artist="artist" />
|
||||
<artist-label
|
||||
v-if="artist"
|
||||
:artist="artist"
|
||||
/>
|
||||
</header>
|
||||
</div>
|
||||
<div
|
||||
|
@ -228,8 +231,9 @@ const remove = async () => {
|
|||
{{ object.title }}
|
||||
</h2>
|
||||
<artist-label
|
||||
class="rounded"
|
||||
v-if="artist"
|
||||
:artist="artist"
|
||||
class="rounded"
|
||||
/>
|
||||
</header>
|
||||
<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>
|
||||
<div v-if="object">
|
||||
<h2 class="ui header">
|
||||
|
@ -20,10 +51,10 @@
|
|||
:limit="50"
|
||||
: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
|
||||
v-for="tracks in discs"
|
||||
:key="tracks.disc_number"
|
||||
:key="getDiscKey(tracks)"
|
||||
>
|
||||
<div class="ui hidden divider" />
|
||||
<play-button
|
||||
|
@ -48,7 +79,7 @@
|
|||
:total="totalTracks"
|
||||
:paginate-by="paginateBy"
|
||||
:page="page"
|
||||
@page-changed="updatePage"
|
||||
@page-changed="emit('page-changed', page)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -64,7 +95,7 @@
|
|||
:total="totalTracks"
|
||||
:paginate-by="paginateBy"
|
||||
:page="page"
|
||||
@page-changed="updatePage"
|
||||
@page-changed="emit('page-changed', page)"
|
||||
/>
|
||||
</template>
|
||||
<template v-if="!artist.channel && !isSerie">
|
||||
|
@ -75,7 +106,7 @@
|
|||
</h2>
|
||||
<library-widget
|
||||
:url="'albums/' + object.id + '/libraries/'"
|
||||
@loaded="$emit('libraries-loaded', $event)"
|
||||
@loaded="emit('libraries-loaded', $event)"
|
||||
>
|
||||
<translate translate-context="Content/Album/Paragraph">
|
||||
This album is present in the following libraries:
|
||||
|
@ -84,41 +115,3 @@
|
|||
</template>
|
||||
</div>
|
||||
</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>
|
||||
<section class="ui vertical stripe segment">
|
||||
<div class="ui text container">
|
||||
|
@ -32,27 +51,3 @@
|
|||
</div>
|
||||
</section>
|
||||
</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>
|
||||
<div v-if="object">
|
||||
<div
|
||||
|
@ -53,9 +104,9 @@
|
|||
</div>
|
||||
<div class="ui hidden divider" />
|
||||
<button
|
||||
v-if="nextAlbumsUrl && loadMoreAlbumsUrl"
|
||||
v-if="loadMoreAlbumsUrl !== null"
|
||||
:class="['ui', {loading: isLoadingMoreAlbums}, 'button']"
|
||||
@click="loadMoreAlbums(loadMoreAlbumsUrl)"
|
||||
@click="loadMoreAlbums()"
|
||||
>
|
||||
<translate translate-context="Content/*/Button.Label">
|
||||
Load more…
|
||||
|
@ -99,56 +150,3 @@
|
|||
</section>
|
||||
</div>
|
||||
</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>
|
||||
<section class="ui vertical stripe segment">
|
||||
<div class="ui text container">
|
||||
|
@ -32,27 +51,3 @@
|
|||
</div>
|
||||
</section>
|
||||
</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>
|
||||
<div class="ui fluid card">
|
||||
<div class="content">
|
||||
|
@ -88,7 +232,7 @@
|
|||
<td>{{ field.id }}</td>
|
||||
|
||||
<td v-if="field.diff">
|
||||
<template v-if="field.config.type === 'attachment' && field.oldRepr">
|
||||
<template v-if="field.config?.type === 'attachment' && field.oldRepr">
|
||||
<img
|
||||
class="ui image"
|
||||
alt=""
|
||||
|
@ -115,7 +259,7 @@
|
|||
v-if="field.diff"
|
||||
:title="field.newRepr"
|
||||
>
|
||||
<template v-if="field.config.type === 'attachment' && field.newRepr">
|
||||
<template v-if="field.config?.type === 'attachment' && field.newRepr">
|
||||
<img
|
||||
class="ui image"
|
||||
alt=""
|
||||
|
@ -136,7 +280,7 @@
|
|||
v-else
|
||||
:title="field.newRepr"
|
||||
>
|
||||
<template v-if="field.config.type === 'attachment' && field.newRepr">
|
||||
<template v-if="field.config?.type === 'attachment' && field.newRepr">
|
||||
<img
|
||||
class="ui image"
|
||||
alt=""
|
||||
|
@ -214,135 +358,3 @@
|
|||
</div>
|
||||
</div>
|
||||
</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
|
||||
}
|
||||
|
||||
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 {
|
||||
uuid: string
|
||||
summary?: string
|
||||
|
|
|
@ -1452,6 +1452,11 @@
|
|||
resolved "https://registry.yarnpkg.com/@types/cookie/-/cookie-0.3.3.tgz#85bc74ba782fb7aa3a514d11767832b0e3bc6803"
|
||||
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":
|
||||
version "2.3.3"
|
||||
resolved "https://registry.yarnpkg.com/@types/dompurify/-/dompurify-2.3.3.tgz#c24c92f698f77ed9cc9d9fa7888f90cf2bfaa23f"
|
||||
|
|
Loading…
Reference in New Issue