Multi-upload to image library #56 (#60)

* feat: support for file upload handler to track multiple files

* feat: update image upload endpoint to allow multiple files

* fix: lint
This commit is contained in:
DecDuck
2025-06-01 16:06:56 +10:00
committed by GitHub
parent 3e5c3678d5
commit 40e66def1e
6 changed files with 42 additions and 22 deletions

View File

@ -51,15 +51,22 @@
class="transition mt-2 block text-sm font-semibold text-zinc-400 group-hover:text-zinc-500"
>Upload file</span
>
<p v-if="currentFile" class="mt-1 text-xs text-zinc-400">
{{ currentFile.name }}
</p>
<div v-if="currentFileList">
<p
v-for="currentFile in currentFileList"
:key="currentFile"
class="mt-1 text-[10px] text-zinc-500 whitespace-nowrap"
>
{{ currentFile }}
</p>
</div>
</label>
<input
id="file-upload"
:accept="props.accept"
class="hidden"
type="file"
:multiple="props.multiple"
@change="(e) => (file = (e.target as any)?.files)"
/>
</div>
@ -67,7 +74,7 @@
</div>
<div class="mt-5 sm:mt-4 sm:flex sm:flex-row-reverse">
<LoadingButton
:disabled="currentFile == undefined"
:disabled="currentFiles == undefined"
type="button"
:loading="uploadLoading"
:class="['inline-flex w-full shadow-sm sm:ml-3 sm:w-auto']"
@ -123,10 +130,19 @@ const open = defineModel<boolean>({
});
const file = ref<FileList | undefined>();
const currentFile = computed(() => file.value?.item(0));
const currentFiles = computed(() => file.value);
const currentFileList = computed(() => {
if (!currentFiles.value) return undefined;
const list = [];
for (const file of currentFiles.value) {
list.push(file.name);
}
return list;
});
const props = defineProps<{
endpoint: string;
accept: string;
multiple?: boolean;
options?: { [key: string]: string };
}>();
const emit = defineEmits(["upload"]);
@ -134,10 +150,12 @@ const emit = defineEmits(["upload"]);
const uploadLoading = ref(false);
const uploadError = ref<string | undefined>();
async function uploadFile() {
if (!currentFile.value) return;
if (!currentFiles.value) return;
const form = new FormData();
form.append("file", currentFile.value);
for (const file of currentFiles.value) {
form.append(file.name, file);
}
if (props.options) {
for (const [key, value] of Object.entries(props.options)) {

View File

@ -293,6 +293,7 @@
:options="{ id: game.id }"
accept="image/*"
endpoint="/api/v1/admin/game/image"
:multiple="true"
@upload="(result: Game) => uploadAfterImageUpload(result)"
/>
<ModalTemplate v-model="showAddCarouselModal">

View File

@ -20,8 +20,8 @@ export default defineEventHandler(async (h3) => {
statusMessage: "Failed to upload file",
});
const [id, options, pull, dump] = uploadResult;
if (!id) {
const [ids, options, pull, dump] = uploadResult;
if (ids.length == 0) {
dump();
throw createError({
statusCode: 400,
@ -48,7 +48,7 @@ export default defineEventHandler(async (h3) => {
},
data: {
mImageLibraryObjectIds: {
push: id,
push: ids,
},
},
});

View File

@ -14,14 +14,16 @@ export default defineEventHandler(async (h3) => {
statusMessage: "This endpoint requires multipart form data.",
});
const uploadResult = await handleFileUpload(h3, {}, ["internal:read"]);
const uploadResult = await handleFileUpload(h3, {}, ["internal:read"], 1);
if (!uploadResult)
throw createError({
statusCode: 400,
statusMessage: "Failed to upload file",
});
const [id, options, pull, dump] = uploadResult;
const [ids, options, pull, dump] = uploadResult;
const id = ids.at(0);
// handleFileUpload reads the rest of the options for us.
const name = options.name;

View File

@ -14,19 +14,20 @@ export default defineEventHandler(async (h3) => {
statusMessage: "This endpoint requires multipart form data.",
});
const uploadResult = await handleFileUpload(h3, {}, ["internal:read"]);
const uploadResult = await handleFileUpload(h3, {}, ["internal:read"], 1);
if (!uploadResult)
throw createError({
statusCode: 400,
statusMessage: "Failed to upload file",
});
const [imageId, options, pull, _dump] = uploadResult;
const [imageIds, options, pull, _dump] = uploadResult;
const title = options.title;
const description = options.description;
const content = options.content;
const tags = options.tags ? (JSON.parse(options.tags) as string[]) : [];
const imageId = imageIds.at(0);
if (!title || !description || !content)
throw createError({

View File

@ -6,23 +6,21 @@ export async function handleFileUpload(
h3: H3Event<EventHandlerRequest>,
metadata: { [key: string]: string },
permissions: Array<string>,
): Promise<
[string | undefined, { [key: string]: string }, Pull, Dump] | undefined
> {
max = -1,
): Promise<[string[], { [key: string]: string }, Pull, Dump] | undefined> {
const formData = await readMultipartFormData(h3);
if (!formData) return undefined;
const transactionalHandler = new ObjectTransactionalHandler();
const [add, pull, dump] = transactionalHandler.new(metadata, permissions);
const options: { [key: string]: string } = {};
let id;
const ids = [];
for (const entry of formData) {
if (entry.filename) {
// Only pick one file
if (id) continue;
if (max > 0 && ids.length >= max) continue;
// Add file to transaction handler so we can void it later if we error out
id = add(entry.data);
ids.push(add(entry.data));
continue;
}
if (!entry.name) continue;
@ -30,5 +28,5 @@ export async function handleFileUpload(
options[entry.name] = entry.data.toString("utf-8");
}
return [id, options, pull, dump];
return [ids, options, pull, dump];
}