route element file uploads
Add short and long PDF paths to elements, keep pdfPath as a short PDF compatibility alias, and route generic file uploads by fileType.
This commit is contained in:
parent
0d5783ba9c
commit
9bbabc7fa6
28 changed files with 686 additions and 214 deletions
|
|
@ -1,4 +1,4 @@
|
|||
import { ref } from 'vue'
|
||||
import { ref, type Ref } from 'vue'
|
||||
import { defineStore } from 'pinia'
|
||||
|
||||
export interface ChildElement {
|
||||
|
|
@ -10,10 +10,14 @@ export interface ChildElement {
|
|||
export interface Element extends ChildElement {
|
||||
iconImageUrl: string | null
|
||||
richText: string
|
||||
shortPdfPath: string | null
|
||||
longPdfPath: string | null
|
||||
pdfPath: string | null
|
||||
youtubeUrl: string | null
|
||||
}
|
||||
|
||||
type ElementFileType = 'iconImage' | 'shortPdf' | 'longPdf'
|
||||
|
||||
interface ElementResponse {
|
||||
element: Element
|
||||
childElements: ChildElement[]
|
||||
|
|
@ -35,6 +39,8 @@ interface ElementPatchInput {
|
|||
description?: string
|
||||
iconImageUrl?: string
|
||||
richText?: string
|
||||
shortPdfPath?: string
|
||||
longPdfPath?: string
|
||||
pdfPath?: string
|
||||
youtubeUrl?: string
|
||||
}
|
||||
|
|
@ -53,7 +59,8 @@ export const useElementsStore = defineStore('elements', () => {
|
|||
const error = ref<string | null>(null)
|
||||
const isSaving = ref(false)
|
||||
const isUploadingIconImage = ref(false)
|
||||
const isUploadingPdf = ref(false)
|
||||
const isUploadingShortPdf = ref(false)
|
||||
const isUploadingLongPdf = ref(false)
|
||||
const saveError = ref<string | null>(null)
|
||||
|
||||
async function fetchElement(elementId: string): Promise<void> {
|
||||
|
|
@ -89,16 +96,35 @@ export const useElementsStore = defineStore('elements', () => {
|
|||
}
|
||||
}
|
||||
|
||||
async function updateElement(elementId: string, input: UpdateElementInput): Promise<boolean> {
|
||||
async function updateElement(
|
||||
elementId: string,
|
||||
input: UpdateElementInput,
|
||||
): Promise<boolean> {
|
||||
return await saveElementPatch(elementId, input, 'Could not save element')
|
||||
}
|
||||
|
||||
async function clearElementIconImage(elementId: string): Promise<boolean> {
|
||||
return await saveElementPatch(elementId, { iconImageUrl: '' }, 'Could not remove icon image')
|
||||
return await saveElementPatch(
|
||||
elementId,
|
||||
{ iconImageUrl: '' },
|
||||
'Could not remove icon image',
|
||||
)
|
||||
}
|
||||
|
||||
async function clearElementPdf(elementId: string): Promise<boolean> {
|
||||
return await saveElementPatch(elementId, { pdfPath: '' }, 'Could not remove PDF')
|
||||
async function clearElementShortPdf(elementId: string): Promise<boolean> {
|
||||
return await saveElementPatch(
|
||||
elementId,
|
||||
{ shortPdfPath: '' },
|
||||
'Could not remove short PDF',
|
||||
)
|
||||
}
|
||||
|
||||
async function clearElementLongPdf(elementId: string): Promise<boolean> {
|
||||
return await saveElementPatch(
|
||||
elementId,
|
||||
{ longPdfPath: '' },
|
||||
'Could not remove long PDF',
|
||||
)
|
||||
}
|
||||
|
||||
async function saveElementPatch(
|
||||
|
|
@ -126,38 +152,64 @@ export const useElementsStore = defineStore('elements', () => {
|
|||
}
|
||||
}
|
||||
|
||||
async function uploadElementIconImage(elementId: string, file: File): Promise<boolean> {
|
||||
saveError.value = null
|
||||
isUploadingIconImage.value = true
|
||||
|
||||
try {
|
||||
const formData = new FormData()
|
||||
formData.append('elementId', elementId)
|
||||
formData.append('iconImage', file)
|
||||
const response = await fetch(ELEMENT_UPDATE_URL, {
|
||||
method: 'POST',
|
||||
headers: { Accept: 'application/json' },
|
||||
credentials: 'include',
|
||||
body: formData,
|
||||
})
|
||||
|
||||
return await handleElementResponse(response, 'Could not upload icon image')
|
||||
} catch {
|
||||
saveError.value = 'Network error - could not upload icon image'
|
||||
return false
|
||||
} finally {
|
||||
isUploadingIconImage.value = false
|
||||
}
|
||||
async function uploadElementIconImage(
|
||||
elementId: string,
|
||||
file: File,
|
||||
): Promise<boolean> {
|
||||
return await uploadElementFile(
|
||||
elementId,
|
||||
file,
|
||||
'iconImage',
|
||||
isUploadingIconImage,
|
||||
'Could not upload icon image',
|
||||
'Network error - could not upload icon image',
|
||||
)
|
||||
}
|
||||
|
||||
async function uploadElementPdf(elementId: string, file: File): Promise<boolean> {
|
||||
async function uploadElementShortPdf(
|
||||
elementId: string,
|
||||
file: File,
|
||||
): Promise<boolean> {
|
||||
return await uploadElementFile(
|
||||
elementId,
|
||||
file,
|
||||
'shortPdf',
|
||||
isUploadingShortPdf,
|
||||
'Could not upload short PDF',
|
||||
'Network error - could not upload short PDF',
|
||||
)
|
||||
}
|
||||
|
||||
async function uploadElementLongPdf(
|
||||
elementId: string,
|
||||
file: File,
|
||||
): Promise<boolean> {
|
||||
return await uploadElementFile(
|
||||
elementId,
|
||||
file,
|
||||
'longPdf',
|
||||
isUploadingLongPdf,
|
||||
'Could not upload long PDF',
|
||||
'Network error - could not upload long PDF',
|
||||
)
|
||||
}
|
||||
|
||||
async function uploadElementFile(
|
||||
elementId: string,
|
||||
file: File,
|
||||
fileType: ElementFileType,
|
||||
isUploadingFile: Ref<boolean>,
|
||||
failureMessage: string,
|
||||
networkErrorMessage: string,
|
||||
): Promise<boolean> {
|
||||
saveError.value = null
|
||||
isUploadingPdf.value = true
|
||||
isUploadingFile.value = true
|
||||
|
||||
try {
|
||||
const formData = new FormData()
|
||||
formData.append('elementId', elementId)
|
||||
formData.append('pdf', file)
|
||||
formData.append('fileType', fileType)
|
||||
formData.append('file', file)
|
||||
const response = await fetch(ELEMENT_UPDATE_URL, {
|
||||
method: 'POST',
|
||||
headers: { Accept: 'application/json' },
|
||||
|
|
@ -165,12 +217,12 @@ export const useElementsStore = defineStore('elements', () => {
|
|||
body: formData,
|
||||
})
|
||||
|
||||
return await handleElementResponse(response, 'Could not upload PDF')
|
||||
return await handleElementResponse(response, failureMessage)
|
||||
} catch {
|
||||
saveError.value = 'Network error - could not upload PDF'
|
||||
saveError.value = networkErrorMessage
|
||||
return false
|
||||
} finally {
|
||||
isUploadingPdf.value = false
|
||||
isUploadingFile.value = false
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -203,7 +255,10 @@ export const useElementsStore = defineStore('elements', () => {
|
|||
return true
|
||||
}
|
||||
|
||||
async function errorMessage(response: Response, fallbackMessage: string): Promise<string> {
|
||||
async function errorMessage(
|
||||
response: Response,
|
||||
fallbackMessage: string,
|
||||
): Promise<string> {
|
||||
try {
|
||||
const data: ErrorResponse = await response.json()
|
||||
return data.error ?? fallbackMessage
|
||||
|
|
@ -219,13 +274,16 @@ export const useElementsStore = defineStore('elements', () => {
|
|||
error,
|
||||
isSaving,
|
||||
isUploadingIconImage,
|
||||
isUploadingPdf,
|
||||
isUploadingShortPdf,
|
||||
isUploadingLongPdf,
|
||||
saveError,
|
||||
fetchElement,
|
||||
updateElement,
|
||||
uploadElementIconImage,
|
||||
uploadElementPdf,
|
||||
uploadElementShortPdf,
|
||||
uploadElementLongPdf,
|
||||
clearElementIconImage,
|
||||
clearElementPdf,
|
||||
clearElementShortPdf,
|
||||
clearElementLongPdf,
|
||||
}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -14,13 +14,23 @@ interface ElementForm {
|
|||
|
||||
const route = useRoute()
|
||||
const elementsStore = useElementsStore()
|
||||
const { element, isLoading, error, isSaving, isUploadingIconImage, isUploadingPdf, saveError } =
|
||||
storeToRefs(elementsStore)
|
||||
const {
|
||||
element,
|
||||
isLoading,
|
||||
error,
|
||||
isSaving,
|
||||
isUploadingIconImage,
|
||||
isUploadingShortPdf,
|
||||
isUploadingLongPdf,
|
||||
saveError,
|
||||
} = storeToRefs(elementsStore)
|
||||
const savedMessage = ref<string | null>(null)
|
||||
const iconImageStatus = ref<string | null>(null)
|
||||
const pdfStatus = ref<string | null>(null)
|
||||
const shortPdfStatus = ref<string | null>(null)
|
||||
const longPdfStatus = ref<string | null>(null)
|
||||
const iconImageInput = ref<HTMLInputElement | null>(null)
|
||||
const pdfInput = ref<HTMLInputElement | null>(null)
|
||||
const shortPdfInput = ref<HTMLInputElement | null>(null)
|
||||
const longPdfInput = ref<HTMLInputElement | null>(null)
|
||||
|
||||
const form = reactive<ElementForm>({
|
||||
title: '',
|
||||
|
|
@ -56,7 +66,8 @@ watch(
|
|||
|
||||
savedMessage.value = null
|
||||
iconImageStatus.value = null
|
||||
pdfStatus.value = null
|
||||
shortPdfStatus.value = null
|
||||
longPdfStatus.value = null
|
||||
void elementsStore.fetchElement(currentElementId)
|
||||
},
|
||||
{ immediate: true },
|
||||
|
|
@ -95,8 +106,12 @@ function chooseIconImage(): void {
|
|||
iconImageInput.value?.click()
|
||||
}
|
||||
|
||||
function choosePdf(): void {
|
||||
pdfInput.value?.click()
|
||||
function chooseShortPdf(): void {
|
||||
shortPdfInput.value?.click()
|
||||
}
|
||||
|
||||
function chooseLongPdf(): void {
|
||||
longPdfInput.value?.click()
|
||||
}
|
||||
|
||||
async function handleIconImageChange(changeEvent: Event): Promise<void> {
|
||||
|
|
@ -107,7 +122,10 @@ async function handleIconImageChange(changeEvent: Event): Promise<void> {
|
|||
|
||||
savedMessage.value = null
|
||||
iconImageStatus.value = null
|
||||
const uploaded = await elementsStore.uploadElementIconImage(elementId.value, selectedFile)
|
||||
const uploaded = await elementsStore.uploadElementIconImage(
|
||||
elementId.value,
|
||||
selectedFile,
|
||||
)
|
||||
resetInput(changeEvent)
|
||||
|
||||
if (uploaded) {
|
||||
|
|
@ -115,19 +133,41 @@ async function handleIconImageChange(changeEvent: Event): Promise<void> {
|
|||
}
|
||||
}
|
||||
|
||||
async function handlePdfChange(changeEvent: Event): Promise<void> {
|
||||
async function handleShortPdfChange(changeEvent: Event): Promise<void> {
|
||||
const selectedFile = selectedInputFile(changeEvent)
|
||||
if (selectedFile === null || elementId.value === '') {
|
||||
return
|
||||
}
|
||||
|
||||
savedMessage.value = null
|
||||
pdfStatus.value = null
|
||||
const uploaded = await elementsStore.uploadElementPdf(elementId.value, selectedFile)
|
||||
shortPdfStatus.value = null
|
||||
const uploaded = await elementsStore.uploadElementShortPdf(
|
||||
elementId.value,
|
||||
selectedFile,
|
||||
)
|
||||
resetInput(changeEvent)
|
||||
|
||||
if (uploaded) {
|
||||
pdfStatus.value = 'PDF updated'
|
||||
shortPdfStatus.value = 'Short PDF updated'
|
||||
}
|
||||
}
|
||||
|
||||
async function handleLongPdfChange(changeEvent: Event): Promise<void> {
|
||||
const selectedFile = selectedInputFile(changeEvent)
|
||||
if (selectedFile === null || elementId.value === '') {
|
||||
return
|
||||
}
|
||||
|
||||
savedMessage.value = null
|
||||
longPdfStatus.value = null
|
||||
const uploaded = await elementsStore.uploadElementLongPdf(
|
||||
elementId.value,
|
||||
selectedFile,
|
||||
)
|
||||
resetInput(changeEvent)
|
||||
|
||||
if (uploaded) {
|
||||
longPdfStatus.value = 'Long PDF updated'
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -144,16 +184,29 @@ async function handleRemoveIconImage(): Promise<void> {
|
|||
}
|
||||
}
|
||||
|
||||
async function handleRemovePdf(): Promise<void> {
|
||||
async function handleRemoveShortPdf(): Promise<void> {
|
||||
if (elementId.value === '') {
|
||||
return
|
||||
}
|
||||
|
||||
savedMessage.value = null
|
||||
pdfStatus.value = null
|
||||
const removed = await elementsStore.clearElementPdf(elementId.value)
|
||||
shortPdfStatus.value = null
|
||||
const removed = await elementsStore.clearElementShortPdf(elementId.value)
|
||||
if (removed) {
|
||||
pdfStatus.value = 'PDF removed'
|
||||
shortPdfStatus.value = 'Short PDF removed'
|
||||
}
|
||||
}
|
||||
|
||||
async function handleRemoveLongPdf(): Promise<void> {
|
||||
if (elementId.value === '') {
|
||||
return
|
||||
}
|
||||
|
||||
savedMessage.value = null
|
||||
longPdfStatus.value = null
|
||||
const removed = await elementsStore.clearElementLongPdf(elementId.value)
|
||||
if (removed) {
|
||||
longPdfStatus.value = 'Long PDF removed'
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -186,7 +239,9 @@ function resetInput(changeEvent: Event): void {
|
|||
<h1 class="admin-element-page__heading">Edit Element</h1>
|
||||
</header>
|
||||
|
||||
<p v-if="isLoading" class="admin-element-page__status">Loading element...</p>
|
||||
<p v-if="isLoading" class="admin-element-page__status">
|
||||
Loading element...
|
||||
</p>
|
||||
<p
|
||||
v-else-if="error"
|
||||
class="admin-element-page__status admin-element-page__status--error"
|
||||
|
|
@ -194,7 +249,11 @@ function resetInput(changeEvent: Event): void {
|
|||
>
|
||||
{{ error }}
|
||||
</p>
|
||||
<form v-else-if="element" class="admin-element-page__form" @submit.prevent="handleSubmit">
|
||||
<form
|
||||
v-else-if="element"
|
||||
class="admin-element-page__form"
|
||||
@submit.prevent="handleSubmit"
|
||||
>
|
||||
<label class="admin-element-page__field">
|
||||
<span class="admin-element-page__label">Title</span>
|
||||
<input
|
||||
|
|
@ -255,7 +314,10 @@ function resetInput(changeEvent: Event): void {
|
|||
</div>
|
||||
<p
|
||||
v-if="iconImageStatus !== null"
|
||||
class="admin-element-page__status admin-element-page__status--inline"
|
||||
:class="[
|
||||
'admin-element-page__status',
|
||||
'admin-element-page__status--inline',
|
||||
]"
|
||||
data-cy="admin-element-icon-image-status"
|
||||
aria-live="polite"
|
||||
>
|
||||
|
|
@ -274,53 +336,110 @@ function resetInput(changeEvent: Event): void {
|
|||
</label>
|
||||
|
||||
<section class="admin-element-page__media-field">
|
||||
<span class="admin-element-page__label">PDF</span>
|
||||
<span class="admin-element-page__label">Short PDF</span>
|
||||
<a
|
||||
v-if="element.pdfPath !== null"
|
||||
:href="element.pdfPath"
|
||||
v-if="element.shortPdfPath !== null"
|
||||
:href="element.shortPdfPath"
|
||||
class="admin-element-page__pdf-link"
|
||||
data-cy="admin-element-current-pdf"
|
||||
data-cy="admin-element-current-short-pdf"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
View current PDF
|
||||
View current short PDF
|
||||
</a>
|
||||
<p v-else class="admin-element-page__empty-media">No PDF</p>
|
||||
<p v-else class="admin-element-page__empty-media">No short PDF</p>
|
||||
<div class="admin-element-page__media-actions">
|
||||
<input
|
||||
ref="pdfInput"
|
||||
ref="shortPdfInput"
|
||||
class="admin-element-page__file-input"
|
||||
data-cy="admin-element-pdf-input"
|
||||
data-cy="admin-element-short-pdf-input"
|
||||
type="file"
|
||||
accept="application/pdf"
|
||||
:disabled="isUploadingPdf"
|
||||
@change="handlePdfChange"
|
||||
:disabled="isUploadingShortPdf"
|
||||
@change="handleShortPdfChange"
|
||||
/>
|
||||
<button
|
||||
class="admin-element-page__secondary-button"
|
||||
type="button"
|
||||
:disabled="isUploadingPdf"
|
||||
@click="choosePdf"
|
||||
:disabled="isUploadingShortPdf"
|
||||
@click="chooseShortPdf"
|
||||
>
|
||||
{{ isUploadingPdf ? 'Uploading...' : 'Choose PDF' }}
|
||||
{{ isUploadingShortPdf ? 'Uploading...' : 'Choose short PDF' }}
|
||||
</button>
|
||||
<button
|
||||
v-if="element.pdfPath !== null"
|
||||
v-if="element.shortPdfPath !== null"
|
||||
class="admin-element-page__secondary-button"
|
||||
type="button"
|
||||
:disabled="isSaving || isUploadingPdf"
|
||||
@click="handleRemovePdf"
|
||||
:disabled="isSaving || isUploadingShortPdf"
|
||||
@click="handleRemoveShortPdf"
|
||||
>
|
||||
Remove PDF
|
||||
Remove short PDF
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
v-if="pdfStatus !== null"
|
||||
class="admin-element-page__status admin-element-page__status--inline"
|
||||
data-cy="admin-element-pdf-status"
|
||||
v-if="shortPdfStatus !== null"
|
||||
:class="[
|
||||
'admin-element-page__status',
|
||||
'admin-element-page__status--inline',
|
||||
]"
|
||||
data-cy="admin-element-short-pdf-status"
|
||||
aria-live="polite"
|
||||
>
|
||||
{{ pdfStatus }}
|
||||
{{ shortPdfStatus }}
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section class="admin-element-page__media-field">
|
||||
<span class="admin-element-page__label">Long PDF</span>
|
||||
<a
|
||||
v-if="element.longPdfPath !== null"
|
||||
:href="element.longPdfPath"
|
||||
class="admin-element-page__pdf-link"
|
||||
data-cy="admin-element-current-long-pdf"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
View current long PDF
|
||||
</a>
|
||||
<p v-else class="admin-element-page__empty-media">No long PDF</p>
|
||||
<div class="admin-element-page__media-actions">
|
||||
<input
|
||||
ref="longPdfInput"
|
||||
class="admin-element-page__file-input"
|
||||
data-cy="admin-element-long-pdf-input"
|
||||
type="file"
|
||||
accept="application/pdf"
|
||||
:disabled="isUploadingLongPdf"
|
||||
@change="handleLongPdfChange"
|
||||
/>
|
||||
<button
|
||||
class="admin-element-page__secondary-button"
|
||||
type="button"
|
||||
:disabled="isUploadingLongPdf"
|
||||
@click="chooseLongPdf"
|
||||
>
|
||||
{{ isUploadingLongPdf ? 'Uploading...' : 'Choose long PDF' }}
|
||||
</button>
|
||||
<button
|
||||
v-if="element.longPdfPath !== null"
|
||||
class="admin-element-page__secondary-button"
|
||||
type="button"
|
||||
:disabled="isSaving || isUploadingLongPdf"
|
||||
@click="handleRemoveLongPdf"
|
||||
>
|
||||
Remove long PDF
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
v-if="longPdfStatus !== null"
|
||||
:class="[
|
||||
'admin-element-page__status',
|
||||
'admin-element-page__status--inline',
|
||||
]"
|
||||
data-cy="admin-element-long-pdf-status"
|
||||
aria-live="polite"
|
||||
>
|
||||
{{ longPdfStatus }}
|
||||
</p>
|
||||
</section>
|
||||
|
||||
|
|
@ -345,7 +464,10 @@ function resetInput(changeEvent: Event): void {
|
|||
</button>
|
||||
<p
|
||||
v-if="statusMessage !== null"
|
||||
:class="['admin-element-page__status', 'admin-element-page__status--inline']"
|
||||
:class="[
|
||||
'admin-element-page__status',
|
||||
'admin-element-page__status--inline',
|
||||
]"
|
||||
data-cy="admin-element-status"
|
||||
aria-live="polite"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -242,15 +242,29 @@ function isShortYoutubeHost(hostname: string): boolean {
|
|||
/>
|
||||
</div>
|
||||
|
||||
<div v-if="element.pdfPath !== null" class="element-page__actions">
|
||||
<div
|
||||
v-if="element.shortPdfPath !== null || element.longPdfPath !== null"
|
||||
class="element-page__actions"
|
||||
>
|
||||
<a
|
||||
:href="element.pdfPath"
|
||||
v-if="element.shortPdfPath !== null"
|
||||
:href="element.shortPdfPath"
|
||||
class="element-page__pdf-link"
|
||||
data-cy="element-pdf-link"
|
||||
data-cy="element-short-pdf-link"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
View PDF
|
||||
View Short PDF
|
||||
</a>
|
||||
<a
|
||||
v-if="element.longPdfPath !== null"
|
||||
:href="element.longPdfPath"
|
||||
class="element-page__pdf-link"
|
||||
data-cy="element-long-pdf-link"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
View Long PDF
|
||||
</a>
|
||||
</div>
|
||||
|
||||
|
|
@ -360,7 +374,9 @@ function isShortYoutubeHost(hostname: string): boolean {
|
|||
|
||||
.element-page__actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
gap: 0.75rem;
|
||||
margin-top: 1.75rem;
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue