Migrate attachment input to new v-model

It also automatically cleans up attachments that users uploaded and decided not to use
environments/review-front-deve-otr6gc/deployments/13419
Kasper Seweryn 2022-05-02 09:36:01 +02:00 zatwierdzone przez Georg Krause
rodzic 2f80e0935f
commit bf009440ff
6 zmienionych plików z 131 dodań i 99 usunięć

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

@ -11,7 +11,6 @@
</div>
<attachment-input
v-model="newValues.cover"
:required="false"
@delete="newValues.cover = null"
>
<translate translate-context="Content/Channel/*">

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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