Migrate some components

This commit is contained in:
wvffle 2022-08-08 22:39:59 +00:00 committed by Georg Krause
parent 7eca32e006
commit c1494c8894
10 changed files with 317 additions and 368 deletions

View File

@ -8,7 +8,7 @@ import { useStore } from '~/store'
import AlbumCard from '~/components/audio/album/Card.vue'
interface Props {
filters: Record<string, string>
filters: Record<string, string | boolean>
showCount?: boolean
search?: boolean
limit?: number

View File

@ -8,7 +8,7 @@ import { useStore } from '~/store'
import ArtistCard from '~/components/audio/artist/Card.vue'
interface Props {
filters: Record<string, string>
filters: Record<string, string | boolean>
search?: boolean
header?: boolean
limit?: number

View File

@ -16,7 +16,7 @@ interface Emits {
}
interface Props {
filters: Record<string, string>
filters: Record<string, string | boolean>
url: string
isActivity?: boolean
showCount?: boolean

View File

@ -1,5 +1,6 @@
<script setup lang="ts">
import type { EditObject, EditObjectType } from '~/composables/moderation/useEditConfigs'
import type { ReviewState } from '~/types'
import axios from 'axios'
import useEditConfigs from '~/composables/moderation/useEditConfigs'
@ -17,7 +18,7 @@ const props = defineProps<Props>()
const configs = useEditConfigs()
const config = computed(() => configs[props.objectType])
const currentState = computed(() => config.value.fields.reduce((state: Record<string, unknown>, field) => {
const currentState = computed(() => config.value.fields.reduce((state: ReviewState, field) => {
state[field.id] = { value: field.getValue(props.object) }
return state
}, {}))

View File

@ -1,3 +1,148 @@
<script setup lang="ts">
import type { EditObject, EditObjectType } from '~/composables/moderation/useEditConfigs'
import type { BackendError, License, ReviewState } from '~/types'
import { computed, onMounted, reactive, ref, watchEffect } from 'vue'
import { isEqual, clone } from 'lodash-es'
import { useGettext } from 'vue3-gettext'
import { useStore } from '~/store'
import axios from 'axios'
import $ from 'jquery'
import AttachmentInput from '~/components/common/AttachmentInput.vue'
import useEditConfigs from '~/composables/moderation/useEditConfigs'
import TagsSelector from '~/components/library/TagsSelector.vue'
import EditList from '~/components/library/EditList.vue'
import EditCard from '~/components/library/EditCard.vue'
interface Props {
objectType: EditObjectType
object: EditObject
licenses: License[]
}
const props = defineProps<Props>()
const { $pgettext } = useGettext()
const configs = useEditConfigs()
const store = useStore()
const config = computed(() => configs[props.objectType])
const currentState = computed(() => config.value.fields.reduce((state: ReviewState, field) => {
state[field.id] = { value: field.getValue(props.object) }
return state
}, {}))
const canEdit = computed(() => {
if (!store.state.auth.authenticated) return false
const isOwner = props.object.attributed_to
// TODO (wvffle): Is it better to compare ids? Is full_username unique?
&& store.state.auth.fullUsername === props.object.attributed_to.full_username
return isOwner || store.state.auth.availablePermissions.library
})
const labels = computed(() => ({
summaryPlaceholder: $pgettext('*/*/Placeholder', 'A short summary describing your changes.')
}))
const mutationsUrl = computed(() => props.objectType === 'track'
? `tracks/${props.object.id}/mutations/`
: props.objectType === 'album'
? `albums/${props.object.id}/mutations/`
: props.objectType === 'artist'
? `artists/${props.object.id}/mutations/`
: ''
)
const mutationPayload = computed(() => {
const changedFields = config.value.fields.filter(f => {
return !isEqual(values[f.id], initialValues[f.id])
})
if (changedFields.length === 0) {
return {}
}
const data = {
type: 'update',
payload: {} as Record<string, unknown>,
summary: summary.value
}
for (const field of changedFields) {
data.payload[field.id] = values[field.id]
}
return data
})
const showPendingReview = ref(true)
const editListFilters = computed(() => showPendingReview.value
? { is_approved: 'null' }
: {}
)
const values = reactive({} as Record<string, any>)
const initialValues = reactive({} as Record<string, any>)
for (const { id, getValue } of config.value.fields) {
values[id] = clone(getValue(props.object))
initialValues[id] = clone(values[id])
}
const license = ref()
watchEffect(() => {
if (values.license === null) {
$(license.value).dropdown('clear')
return
}
$(license.value).dropdown('set selected', values.license)
})
onMounted(() => {
$('.ui.dropdown').dropdown({ fullTextSearch: true })
})
const submittedMutation = ref()
const summary = ref('')
const errors = ref([] as string[])
const isLoading = ref(false)
const submit = async () => {
const url = mutationsUrl.value
if (!url) return
isLoading.value = true
errors.value = []
try {
const response = await axios.post(url, {
...mutationPayload.value,
is_approved: canEdit.value
? true
: undefined
})
submittedMutation.value = response.data
} catch (error) {
errors.value = (error as BackendError).backendErrors
}
isLoading.value = false
}
const fieldValuesChanged = (fieldId: string) => {
return !isEqual(values[fieldId], initialValues[fieldId])
}
const resetField = (fieldId: string) => {
values[fieldId] = clone(initialValues[fieldId])
}
</script>
<template>
<div v-if="submittedMutation">
<div class="ui positive message">
@ -107,7 +252,7 @@
:id="fieldConfig.id"
v-model="values[fieldConfig.id]"
:type="fieldConfig.inputType || 'text'"
:required="fieldConfig.required || null"
:required="fieldConfig.required"
:name="fieldConfig.id"
>
</template>
@ -118,7 +263,7 @@
:id="fieldConfig.id"
ref="license"
v-model="values[fieldConfig.id]"
:required="fieldConfig.required || null"
:required="fieldConfig.required"
class="ui fluid search dropdown"
>
<option :value="null">
@ -158,7 +303,7 @@
:id="fieldConfig.id"
v-model="values[fieldConfig.id]"
:initial-value="initialValues[fieldConfig.id]"
:required="fieldConfig.required || null"
:required="fieldConfig.required"
:name="fieldConfig.id"
@delete="values[fieldConfig.id] = initialValues[fieldConfig.id]"
>
@ -220,7 +365,7 @@
<button
:class="['ui', {'loading': isLoading}, 'right', 'floated', 'success', 'button']"
type="submit"
:disabled="isLoading || !mutationPayload || null"
:disabled="isLoading || !mutationPayload"
>
<translate
v-if="canEdit"
@ -238,153 +383,3 @@
</form>
</div>
</template>
<script>
import $ from 'jquery'
import { isEqual, clone } from 'lodash-es'
import axios from 'axios'
import AttachmentInput from '~/components/common/AttachmentInput.vue'
import EditList from '~/components/library/EditList.vue'
import EditCard from '~/components/library/EditCard.vue'
import TagsSelector from '~/components/library/TagsSelector.vue'
import useEditConfigs from '~/composables/moderation/useEditConfigs'
import { computed } from 'vue'
export default {
components: {
EditList,
EditCard,
TagsSelector,
AttachmentInput
},
props: {
objectType: { type: String, required: true },
object: { type: Object, required: true },
licenses: { type: Array, required: true }
},
setup (props) {
const configs = useEditConfigs()
const config = computed(() => configs[props.objectType])
const currentState = computed(() => config.value.fields.reduce((state/*: Record<string, unknown> */, field) => {
state[field.id] = { value: field.getValue(props.object) }
return state
}, {}))
return { config, currentState, configs }
},
data () {
return {
isLoading: false,
errors: [],
values: {},
initialValues: {},
summary: '',
submittedMutation: null,
showPendingReview: true
}
},
computed: {
canEdit () {
if (!this.$store.state.auth.authenticated) return false
const isOwner = this.object.attributed_to
// TODO (wvffle): Is it better to compare ids? Is full_username unique?
&& this.$store.state.auth.fullUsername === this.object.attributed_to.full_username
return isOwner || this.$store.state.auth.availablePermissions.library
},
labels () {
return {
summaryPlaceholder: this.$pgettext('*/*/Placeholder', 'A short summary describing your changes.')
}
},
mutationsUrl () {
if (this.objectType === 'track') {
return `tracks/${this.object.id}/mutations/`
}
if (this.objectType === 'album') {
return `albums/${this.object.id}/mutations/`
}
if (this.objectType === 'artist') {
return `artists/${this.object.id}/mutations/`
}
return null
},
mutationPayload () {
const self = this
const changedFields = this.config.fields.filter(f => {
return !isEqual(self.values[f.id], self.initialValues[f.id])
})
if (changedFields.length === 0) {
return null
}
const payload = {
type: 'update',
payload: {},
summary: this.summary
}
changedFields.forEach((f) => {
payload.payload[f.id] = self.values[f.id]
})
return payload
},
editListFilters () {
if (this.showPendingReview) {
return { is_approved: 'null' }
} else {
return {}
}
}
},
watch: {
'values.license' (newValue) {
if (newValue === null) {
$(this.$refs.license).dropdown('clear')
} else {
$(this.$refs.license).dropdown('set selected', newValue)
}
}
},
created () {
this.setValues()
},
mounted () {
$('.ui.dropdown').dropdown({ fullTextSearch: true })
},
methods: {
setValues () {
for (const { id, getValue } of this.config.fields) {
this.values[id] = clone(getValue(this.object))
this.initialValues[id] = clone(this.values[id])
}
},
submit () {
const self = this
self.isLoading = true
self.errors = []
const payload = clone(this.mutationPayload || {})
if (this.canEdit) {
payload.is_approved = true
}
return axios.post(this.mutationsUrl, payload).then(
response => {
self.isLoading = false
self.submittedMutation = response.data
},
error => {
self.errors = error.backendErrors
self.isLoading = false
}
)
},
fieldValuesChanged (fieldId) {
return !isEqual(this.values[fieldId], this.initialValues[fieldId])
},
resetField (fieldId) {
this.values[fieldId] = clone(this.initialValues[fieldId])
}
}
}
</script>

View File

@ -1,3 +1,23 @@
<script setup lang="ts">
import { computed } from 'vue'
import ChannelsWidget from '~/components/audio/ChannelsWidget.vue'
import TrackWidget from '~/components/audio/track/Widget.vue'
import AlbumWidget from '~/components/audio/album/Widget.vue'
import ArtistWidget from '~/components/audio/artist/Widget.vue'
import RadioButton from '~/components/radios/Button.vue'
interface Props {
id: string
}
const props = defineProps<Props>()
const labels = computed(() => ({
title: `#${props.id}`
}))
</script>
<template>
<main v-title="labels.title">
<section class="ui vertical stripe segment">
@ -87,38 +107,3 @@
</section>
</main>
</template>
<script>
import ChannelsWidget from '~/components/audio/ChannelsWidget.vue'
import TrackWidget from '~/components/audio/track/Widget.vue'
import AlbumWidget from '~/components/audio/album/Widget.vue'
import ArtistWidget from '~/components/audio/artist/Widget.vue'
import RadioButton from '~/components/radios/Button.vue'
export default {
components: {
ArtistWidget,
AlbumWidget,
TrackWidget,
RadioButton,
ChannelsWidget
},
props: {
id: { type: String, required: true }
},
computed: {
labels () {
const title = `#${this.id}`
return {
title
}
},
isAuthenticated () {
return this.$store.state.auth.authenticated
},
hasFavorites () {
return this.$store.state.favorites.count > 0
}
}
}
</script>

View File

@ -1,3 +1,91 @@
<script setup lang="ts">
import type { Tag } from '~/types'
import { ref, watch, onMounted, nextTick } from 'vue'
import { isEqual } from 'lodash-es'
import { useStore } from '~/store'
import $ from 'jquery'
interface Emits {
(e: 'update:modelValue', tags: Tag[]): void
}
interface Props {
modelValue: Tag[]
}
const emit = defineEmits<Emits>()
const props = defineProps<Props>()
const store = useStore()
const dropdown = ref()
watch(() => props.modelValue, (value) => {
const current = $(dropdown.value).dropdown('get value').split(',').sort()
if (!isEqual([...value].sort(), current)) {
$(dropdown.value).dropdown('set exactly', value)
}
})
const handleUpdate = () => {
const value = $(dropdown.value).dropdown('get value').split(',')
emit('update:modelValue', value)
return value
}
onMounted(async () => {
await nextTick()
$(dropdown.value).dropdown({
keys: { delimiter: 32 },
forceSelection: false,
saveRemoteData: false,
filterRemoteData: true,
preserveHTML: false,
apiSettings: {
url: store.getters['instance/absoluteUrl']('/api/v1/tags/?name__startswith={query}&ordering=length&page_size=5'),
beforeXHR: function (xhrObject) {
if (store.state.auth.oauth.accessToken) {
xhrObject.setRequestHeader('Authorization', store.getters['auth/header'])
}
return xhrObject
},
onResponse (response) {
response = { results: [], ...response }
// @ts-expect-error Semantic UI
const currentSearch: string = $(dropdown.value).dropdown('get query')
if (currentSearch) {
const existingTag = response.results.find((result: Tag) => result.name === currentSearch)
if (existingTag) {
if (response.results.indexOf(existingTag) !== 0) {
response.results = [existingTag, ...response.results]
response.results.splice(response.results.indexOf(existingTag) + 1, 1)
}
} else {
response.results = [{ name: currentSearch }, ...response.results]
}
}
return response
}
},
fields: { remoteValues: 'results', value: 'name' },
allowAdditions: true,
minCharacters: 1,
onAdd: handleUpdate,
onRemove: handleUpdate,
onLabelRemove: handleUpdate,
onChange: handleUpdate
})
$(dropdown.value).dropdown('set exactly', props.modelValue)
})
</script>
<template>
<div
ref="dropdown"
@ -17,86 +105,3 @@
</div>
</div>
</template>
<script>
import $ from 'jquery'
import { isEqual } from 'lodash-es'
export default {
props: { modelValue: { type: Array, required: true } },
watch: {
modelValue: {
handler (v) {
const current = $(this.$refs.dropdown).dropdown('get value').split(',').sort()
if (!isEqual([...v].sort(), current)) {
$(this.$refs.dropdown).dropdown('set exactly', v)
}
},
deep: true
}
},
mounted () {
this.$nextTick(() => {
this.initDropdown()
})
},
methods: {
initDropdown () {
const self = this
const handleUpdate = () => {
const value = $(self.$refs.dropdown).dropdown('get value').split(',')
self.$emit('update:modelValue', value)
return value
}
const settings = {
keys: {
delimiter: 32
},
forceSelection: false,
saveRemoteData: false,
filterRemoteData: true,
preserveHTML: false,
apiSettings: {
url: this.$store.getters['instance/absoluteUrl']('/api/v1/tags/?name__startswith={query}&ordering=length&page_size=5'),
beforeXHR: function (xhrObject) {
if (self.$store.state.auth.oauth.accessToken) {
xhrObject.setRequestHeader('Authorization', self.$store.getters['auth/header'])
}
return xhrObject
},
onResponse (response) {
const currentSearch = $(self.$refs.dropdown).dropdown('get query')
response = {
results: [],
...response
}
if (currentSearch) {
const existingTag = response.results.find((result) => result.name === currentSearch)
if (existingTag) {
if (response.results.indexOf(existingTag) !== 0) {
response.results = [existingTag, ...response.results]
response.results.splice(response.results.indexOf(existingTag) + 1, 1)
}
} else {
response.results = [{ name: currentSearch }, ...response.results]
}
}
return response
}
},
fields: {
remoteValues: 'results',
value: 'name'
},
allowAdditions: true,
minCharacters: 1,
onAdd: handleUpdate,
onRemove: handleUpdate,
onLabelRemove: handleUpdate,
onChange: handleUpdate
}
$(this.$refs.dropdown).dropdown(settings)
$(this.$refs.dropdown).dropdown('set exactly', this.modelValue)
}
}
}
</script>

View File

@ -1,3 +1,49 @@
<script setup lang="ts">
import type { Track } from '~/types'
import { humanSize, momentFormat, truncate } from '~/utils/filters'
import { computed, ref, watchEffect } from 'vue'
import time from '~/utils/time'
import axios from 'axios'
import LibraryWidget from '~/components/federation/LibraryWidget.vue'
import PlaylistWidget from '~/components/playlists/Widget.vue'
import TagsList from '~/components/tags/List.vue'
interface Props {
track: Track
}
const props = defineProps<Props>()
const musicbrainzUrl = computed(() => props.track.mbid
? `https://musicbrainz.org/recording/${props.track.mbid}`
: null
)
const upload = computed(() => props.track.uploads?.[0] ?? null)
const license = ref()
const fetchLicense = async (licenseId: string) => {
license.value = undefined
try {
const response = await axios.get(`licenses/${licenseId}`)
license.value = response.data
} catch (error) {
// TODO (wvffle): Handle error
}
}
watchEffect(() => {
if (props.track.license) {
// @ts-expect-error For some reason, track.license is id instead of License here
fetchLicense(props.track.license)
}
})
</script>
<template>
<div v-if="track">
<section class="ui vertical stripe segment">
@ -24,7 +70,7 @@
>
<h3 class="ui header">
<translate
v-if="track.artist.content_category === 'music'"
v-if="track.artist?.content_category === 'music'"
translate-context="Content/*/*"
>
Track Details
@ -148,8 +194,8 @@
</translate>
</td>
<td class="right aligned">
<router-link :to="{name: 'library.artists.detail', params: {id: track.artist.id}}">
{{ track.artist.name }}
<router-link :to="{name: 'library.artists.detail', params: {id: track.artist?.id}}">
{{ track.artist?.name }}
</router-link>
</td>
</tr>
@ -182,7 +228,7 @@
</td>
<td class="right aligned">
<template v-if="track.album && track.album.release_date">
{{ momentFormat(track.album.release_date, 'Y') }}
{{ momentFormat(new Date(track.album.release_date), 'Y') }}
</template>
<template v-else>
<translate translate-context="*/*/*">
@ -273,7 +319,7 @@
</translate>
</h2>
<library-widget
:url="'tracks/' + id + '/libraries/'"
:url="`tracks/${track.id}/libraries/`"
@loaded="$emit('libraries-loaded', $event)"
>
<translate translate-context="Content/Track/Paragraph">
@ -285,88 +331,3 @@
</section>
</div>
</template>
<script>
import axios from 'axios'
import LibraryWidget from '~/components/federation/LibraryWidget.vue'
import TagsList from '~/components/tags/List.vue'
import PlaylistWidget from '~/components/playlists/Widget.vue'
import { humanSize, momentFormat, truncate } from '~/utils/filters'
import time from '~/utils/time'
export default {
components: {
LibraryWidget,
TagsList,
PlaylistWidget
},
props: {
track: { type: Object, required: true },
libraries: { type: Array, default: null }
},
setup () {
return { humanSize, momentFormat, time, truncate }
},
data () {
return {
id: this.track.id,
licenseData: null
}
},
computed: {
labels () {
return {
title: this.$pgettext('*/*/*/Noun', 'Track')
}
},
musicbrainzUrl () {
if (this.track.mbid) {
return 'https://musicbrainz.org/recording/' + this.track.mbid
}
return null
},
upload () {
if (this.track.uploads) {
return this.track.uploads[0]
}
return null
},
license () {
if (!this.track || !this.track.license) {
return null
}
return this.licenseData
},
cover () {
if (this.track.cover && this.track.cover.urls.original) {
return this.track.cover
}
if (this.track.album && this.track.album.cover) {
return this.track.album.cover
}
return null
}
},
watch: {
track (v) {
if (v && v.license) {
this.fetchLicenseData(v.license)
}
}
},
created () {
if (this.track && this.track.license) {
this.fetchLicenseData(this.track.license)
}
},
methods: {
fetchLicenseData (licenseId) {
const self = this
const url = `licenses/${licenseId}`
axios.get(url).then(response => {
self.licenseData = response.data
})
}
}
}
</script>

View File

@ -1,4 +1,4 @@
import type { Album, Artist, Content, Track } from '~/types'
import type { Album, Artist, Content, Track, Actor } from '~/types'
import { gettext } from '~/init/locale'
@ -16,7 +16,7 @@ export interface EditableConfigField extends ConfigField {
id: EditObjectType
}
export type EditObject = Artist | Album | Track
export type EditObject = (Partial<Artist> | Partial<Album> | Partial<Track>) & { attributed_to: Actor }
export type EditObjectType = 'artist' | 'album' | 'track'
type Configs = Record<EditObjectType, { fields: (EditableConfigField|ConfigField)[] }>

View File

@ -96,6 +96,7 @@ export interface Track {
license?: License
tags: string[]
uploads: Upload[]
downloads_count: number
album?: Album
artist?: Artist
@ -277,6 +278,7 @@ export interface Upload {
mimetype: string
extension: string
listen_url: string
bitrate?: number
size?: number
import_status: ImportStatus