kopia lustrzana https://dev.funkwhale.audio/funkwhale/funkwhale
Migrate attachment input to new v-model
It also automatically cleans up attachments that users uploaded and decided not to useenvironments/review-front-deve-otr6gc/deployments/13419
rodzic
2f80e0935f
commit
bf009440ff
|
@ -95,7 +95,6 @@
|
|||
<div class="six wide column">
|
||||
<attachment-input
|
||||
v-model="newValues.cover"
|
||||
:required="false"
|
||||
:image-class="newValues.content_category === 'podcast' ? '' : 'circular'"
|
||||
@delete="newValues.cover = null"
|
||||
>
|
||||
|
|
|
@ -109,12 +109,10 @@
|
|||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
{{ }}
|
||||
<attachment-input
|
||||
:value="avatar.uuid"
|
||||
v-model="avatar.uuid"
|
||||
:initial-value="initialAvatar"
|
||||
:required="false"
|
||||
@input="submitAvatar($event)"
|
||||
@update:model-value="submitAvatar($event)"
|
||||
@delete="avatar = {uuid: null}"
|
||||
>
|
||||
<translate translate-context="Content/Channel/*">
|
||||
|
@ -739,7 +737,7 @@ export default {
|
|||
// properties that will be used in it
|
||||
old_password: '',
|
||||
new_password: '',
|
||||
avatar: { ...(this.$store.state.auth.profile.avatar || { uuid: null }) },
|
||||
avatar: { ...(this.$store.state.auth.profile?.avatar ?? { uuid: null }) },
|
||||
passwordError: '',
|
||||
password: '',
|
||||
isLoading: false,
|
||||
|
|
|
@ -11,7 +11,6 @@
|
|||
</div>
|
||||
<attachment-input
|
||||
v-model="newValues.cover"
|
||||
:required="false"
|
||||
@delete="newValues.cover = null"
|
||||
>
|
||||
<translate translate-context="Content/Channel/*">
|
||||
|
|
|
@ -1,3 +1,93 @@
|
|||
<script setup lang="ts">
|
||||
import axios from 'axios'
|
||||
import { useVModel } from '@vueuse/core'
|
||||
import {reactive, ref, watch, watchEffect} from 'vue'
|
||||
import { BackendError } from '~/types'
|
||||
|
||||
interface Props {
|
||||
modelValue: string
|
||||
imageClass?: string
|
||||
required?: boolean
|
||||
name?: string | undefined
|
||||
initialValue?: string | undefined
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
imageClass: '',
|
||||
required: false,
|
||||
name: undefined,
|
||||
initialValue: undefined
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue', 'delete'])
|
||||
const value = useVModel(props, 'modelValue', emit)
|
||||
|
||||
const attachment = ref()
|
||||
const isLoading = ref(false)
|
||||
const errors = reactive<string[]>([])
|
||||
const attachmentId = Math.random().toString(36).substring(7)
|
||||
|
||||
const input = ref()
|
||||
const file = ref()
|
||||
const submit = async () => {
|
||||
isLoading.value = true
|
||||
errors.length = 0
|
||||
file.value = input.value.files[0]
|
||||
|
||||
const formData = new FormData()
|
||||
formData.append('file', file.value)
|
||||
|
||||
try {
|
||||
const { data } = await axios.post('attachments/', formData, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
})
|
||||
|
||||
attachment.value = data
|
||||
value.value = data.uuid
|
||||
} catch (error) {
|
||||
if (error as BackendError) {
|
||||
const { backendErrors } = error as BackendError
|
||||
errors.push(...backendErrors)
|
||||
}
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const remove = async (uuid: string, sendEvent = true) => {
|
||||
isLoading.value = true
|
||||
errors.length = 0
|
||||
|
||||
try {
|
||||
await axios.delete(`attachments/${uuid}/`)
|
||||
attachment.value = null
|
||||
|
||||
if (sendEvent) emit('delete')
|
||||
} catch (error) {
|
||||
if (error as BackendError) {
|
||||
const { backendErrors } = error as BackendError
|
||||
errors.push(...backendErrors)
|
||||
}
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const initialValue = ref(props.initialValue ?? props.modelValue)
|
||||
watch(value, (to, from) => {
|
||||
// NOTE: Remove old attachment if it's not the original one
|
||||
if (from !== initialValue.value) {
|
||||
remove(from, false)
|
||||
}
|
||||
|
||||
// NOTE: We want to bring back the original attachment, let's delete the current one
|
||||
if (attachment.value && to === initialValue.value) {
|
||||
remove(attachment.value.uuid)
|
||||
}
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="ui form">
|
||||
<div
|
||||
|
@ -49,10 +139,12 @@
|
|||
</label>
|
||||
<input
|
||||
:id="attachmentId"
|
||||
ref="attachment"
|
||||
ref="input"
|
||||
:name="name"
|
||||
:required="required || null"
|
||||
class="ui input"
|
||||
type="file"
|
||||
accept="image/x-png,image/jpeg"
|
||||
accept="image/png,image/jpeg"
|
||||
@change="submit"
|
||||
>
|
||||
</div>
|
||||
|
@ -86,74 +178,3 @@
|
|||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import axios from 'axios'
|
||||
|
||||
export default {
|
||||
props: {
|
||||
value: { type: String, default: null },
|
||||
imageClass: { type: String, default: '', required: false }
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
attachment: null,
|
||||
isLoading: false,
|
||||
errors: [],
|
||||
initialValue: this.value,
|
||||
attachmentId: Math.random().toString(36).substring(7)
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
value (v) {
|
||||
if (this.attachment && v === this.initialValue) {
|
||||
// we had a reset to initial value
|
||||
this.remove(this.attachment.uuid)
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
submit () {
|
||||
this.isLoading = true
|
||||
this.errors = []
|
||||
const self = this
|
||||
this.file = this.$refs.attachment.files[0]
|
||||
const formData = new FormData()
|
||||
formData.append('file', this.file)
|
||||
axios
|
||||
.post('attachments/', formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data'
|
||||
}
|
||||
})
|
||||
.then(
|
||||
response => {
|
||||
this.isLoading = false
|
||||
self.attachment = response.data
|
||||
self.$emit('input', self.attachment.uuid)
|
||||
},
|
||||
error => {
|
||||
self.isLoading = false
|
||||
self.errors = error.backendErrors
|
||||
}
|
||||
)
|
||||
},
|
||||
remove (uuid) {
|
||||
this.isLoading = true
|
||||
this.errors = []
|
||||
const self = this
|
||||
axios.delete(`attachments/${uuid}/`)
|
||||
.then(
|
||||
response => {
|
||||
this.isLoading = false
|
||||
self.attachment = null
|
||||
self.$emit('delete')
|
||||
},
|
||||
error => {
|
||||
self.isLoading = false
|
||||
self.errors = error.backendErrors
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { InitModule } from '~/types'
|
||||
import { BackendError, InitModule, RateLimitStatus } from '~/types'
|
||||
|
||||
import createAuthRefreshInterceptor from 'axios-auth-refresh'
|
||||
import axios, { AxiosError } from 'axios'
|
||||
|
@ -28,36 +28,35 @@ export const install: InitModule = ({ store, router }) => {
|
|||
// Add a response interceptor
|
||||
axios.interceptors.response.use(function (response) {
|
||||
return response
|
||||
}, async (error) => {
|
||||
}, async (error: BackendError) => {
|
||||
error.backendErrors = []
|
||||
if (store.state.auth.authenticated && !store.state.auth.oauth.accessToken && error.response.status === 401) {
|
||||
if (store.state.auth.authenticated && !store.state.auth.oauth.accessToken && error.response?.status === 401) {
|
||||
store.commit('auth/authenticated', false)
|
||||
logger.warn('Received 401 response from API, redirecting to login form', router.currentRoute.value.fullPath)
|
||||
await router.push({ name: 'login', query: { next: router.currentRoute.value.fullPath } })
|
||||
}
|
||||
|
||||
if (error.response.status === 404) {
|
||||
if (error.response?.status === 404) {
|
||||
error.backendErrors.push('Resource not found')
|
||||
const message = error.response.data
|
||||
const message = error.response?.data
|
||||
store.commit('ui/addMessage', {
|
||||
content: message,
|
||||
class: 'error'
|
||||
})
|
||||
} else if (error.response.status === 403) {
|
||||
} else if (error.response?.status === 403) {
|
||||
error.backendErrors.push('Permission denied')
|
||||
} else if (error.response.status === 429) {
|
||||
} else if (error.response?.status === 429) {
|
||||
let message
|
||||
const rateLimitStatus = {
|
||||
limit: error.response.headers['x-ratelimit-limit'],
|
||||
scope: error.response.headers['x-ratelimit-scope'],
|
||||
remaining: error.response.headers['x-ratelimit-remaining'],
|
||||
duration: error.response.headers['x-ratelimit-duration'],
|
||||
availableSeconds: error.response.headers['retry-after'],
|
||||
reset: error.response.headers['x-ratelimit-reset'],
|
||||
resetSeconds: error.response.headers['x-ratelimit-resetseconds']
|
||||
const rateLimitStatus: RateLimitStatus = {
|
||||
limit: error.response?.headers['x-ratelimit-limit'],
|
||||
scope: error.response?.headers['x-ratelimit-scope'],
|
||||
remaining: error.response?.headers['x-ratelimit-remaining'],
|
||||
duration: error.response?.headers['x-ratelimit-duration'],
|
||||
availableSeconds: parseInt(error.response?.headers['retry-after'] ?? 60),
|
||||
reset: error.response?.headers['x-ratelimit-reset'],
|
||||
resetSeconds: error.response?.headers['x-ratelimit-resetseconds']
|
||||
}
|
||||
if (rateLimitStatus.availableSeconds) {
|
||||
rateLimitStatus.availableSeconds = parseInt(rateLimitStatus.availableSeconds)
|
||||
const tryAgain = moment().add(rateLimitStatus.availableSeconds, 's').toNow(true)
|
||||
message = $pgettext('*/Error/Paragraph', 'You sent too many requests and have been rate limited, please try again in %{ delay }')
|
||||
message = $gettext(message, { delay: tryAgain })
|
||||
|
@ -71,10 +70,10 @@ export const install: InitModule = ({ store, router }) => {
|
|||
class: 'error'
|
||||
})
|
||||
logger.error('This client is rate-limited!', rateLimitStatus)
|
||||
} else if (error.response.status === 500) {
|
||||
} else if (error.response?.status === 500) {
|
||||
error.backendErrors.push('A server error occurred')
|
||||
} else if (error.response.data) {
|
||||
if (error.response.data.detail) {
|
||||
} else if (error.response?.data) {
|
||||
if (error.response?.data.detail) {
|
||||
error.backendErrors.push(error.response.data.detail)
|
||||
} else {
|
||||
error.rawPayload = error.response.data
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import type { App } from 'vue'
|
||||
import type { Store } from 'vuex'
|
||||
import { Router } from 'vue-router'
|
||||
import {AxiosError} from "axios";
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
|
@ -47,6 +48,21 @@ export interface APIErrorResponse {
|
|||
[key: string]: APIErrorResponse | string[]
|
||||
}
|
||||
|
||||
export interface BackendError extends AxiosError {
|
||||
backendErrors: string[]
|
||||
rawPayload?: object
|
||||
}
|
||||
|
||||
export interface RateLimitStatus {
|
||||
limit: string
|
||||
scope: string
|
||||
remaining: string
|
||||
duration: string
|
||||
availableSeconds: number
|
||||
reset: string
|
||||
resetSeconds: string
|
||||
}
|
||||
|
||||
// WebSocket stuff
|
||||
export interface PendingReviewEditsWSEvent {
|
||||
pending_review_count: number
|
||||
|
|
Ładowanie…
Reference in New Issue