Migrate some components

This commit is contained in:
wvffle 2022-08-05 21:40:58 +00:00 committed by Georg Krause
parent b3a08c8688
commit 5b95a8f954
11 changed files with 381 additions and 365 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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