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
+25 -7
View File
@@ -51,15 +51,22 @@
class="transition mt-2 block text-sm font-semibold text-zinc-400 group-hover:text-zinc-500" class="transition mt-2 block text-sm font-semibold text-zinc-400 group-hover:text-zinc-500"
>Upload file</span >Upload file</span
> >
<p v-if="currentFile" class="mt-1 text-xs text-zinc-400"> <div v-if="currentFileList">
{{ currentFile.name }} <p
</p> v-for="currentFile in currentFileList"
:key="currentFile"
class="mt-1 text-[10px] text-zinc-500 whitespace-nowrap"
>
{{ currentFile }}
</p>
</div>
</label> </label>
<input <input
id="file-upload" id="file-upload"
:accept="props.accept" :accept="props.accept"
class="hidden" class="hidden"
type="file" type="file"
:multiple="props.multiple"
@change="(e) => (file = (e.target as any)?.files)" @change="(e) => (file = (e.target as any)?.files)"
/> />
</div> </div>
@@ -67,7 +74,7 @@
</div> </div>
<div class="mt-5 sm:mt-4 sm:flex sm:flex-row-reverse"> <div class="mt-5 sm:mt-4 sm:flex sm:flex-row-reverse">
<LoadingButton <LoadingButton
:disabled="currentFile == undefined" :disabled="currentFiles == undefined"
type="button" type="button"
:loading="uploadLoading" :loading="uploadLoading"
:class="['inline-flex w-full shadow-sm sm:ml-3 sm:w-auto']" :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 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<{ const props = defineProps<{
endpoint: string; endpoint: string;
accept: string; accept: string;
multiple?: boolean;
options?: { [key: string]: string }; options?: { [key: string]: string };
}>(); }>();
const emit = defineEmits(["upload"]); const emit = defineEmits(["upload"]);
@@ -134,10 +150,12 @@ const emit = defineEmits(["upload"]);
const uploadLoading = ref(false); const uploadLoading = ref(false);
const uploadError = ref<string | undefined>(); const uploadError = ref<string | undefined>();
async function uploadFile() { async function uploadFile() {
if (!currentFile.value) return; if (!currentFiles.value) return;
const form = new FormData(); const form = new FormData();
form.append("file", currentFile.value); for (const file of currentFiles.value) {
form.append(file.name, file);
}
if (props.options) { if (props.options) {
for (const [key, value] of Object.entries(props.options)) { for (const [key, value] of Object.entries(props.options)) {
@@ -293,6 +293,7 @@
:options="{ id: game.id }" :options="{ id: game.id }"
accept="image/*" accept="image/*"
endpoint="/api/v1/admin/game/image" endpoint="/api/v1/admin/game/image"
:multiple="true"
@upload="(result: Game) => uploadAfterImageUpload(result)" @upload="(result: Game) => uploadAfterImageUpload(result)"
/> />
<ModalTemplate v-model="showAddCarouselModal"> <ModalTemplate v-model="showAddCarouselModal">
+3 -3
View File
@@ -20,8 +20,8 @@ export default defineEventHandler(async (h3) => {
statusMessage: "Failed to upload file", statusMessage: "Failed to upload file",
}); });
const [id, options, pull, dump] = uploadResult; const [ids, options, pull, dump] = uploadResult;
if (!id) { if (ids.length == 0) {
dump(); dump();
throw createError({ throw createError({
statusCode: 400, statusCode: 400,
@@ -48,7 +48,7 @@ export default defineEventHandler(async (h3) => {
}, },
data: { data: {
mImageLibraryObjectIds: { mImageLibraryObjectIds: {
push: id, push: ids,
}, },
}, },
}); });
+4 -2
View File
@@ -14,14 +14,16 @@ export default defineEventHandler(async (h3) => {
statusMessage: "This endpoint requires multipart form data.", statusMessage: "This endpoint requires multipart form data.",
}); });
const uploadResult = await handleFileUpload(h3, {}, ["internal:read"]); const uploadResult = await handleFileUpload(h3, {}, ["internal:read"], 1);
if (!uploadResult) if (!uploadResult)
throw createError({ throw createError({
statusCode: 400, statusCode: 400,
statusMessage: "Failed to upload file", 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. // handleFileUpload reads the rest of the options for us.
const name = options.name; const name = options.name;
+3 -2
View File
@@ -14,19 +14,20 @@ export default defineEventHandler(async (h3) => {
statusMessage: "This endpoint requires multipart form data.", statusMessage: "This endpoint requires multipart form data.",
}); });
const uploadResult = await handleFileUpload(h3, {}, ["internal:read"]); const uploadResult = await handleFileUpload(h3, {}, ["internal:read"], 1);
if (!uploadResult) if (!uploadResult)
throw createError({ throw createError({
statusCode: 400, statusCode: 400,
statusMessage: "Failed to upload file", statusMessage: "Failed to upload file",
}); });
const [imageId, options, pull, _dump] = uploadResult; const [imageIds, options, pull, _dump] = uploadResult;
const title = options.title; const title = options.title;
const description = options.description; const description = options.description;
const content = options.content; const content = options.content;
const tags = options.tags ? (JSON.parse(options.tags) as string[]) : []; const tags = options.tags ? (JSON.parse(options.tags) as string[]) : [];
const imageId = imageIds.at(0);
if (!title || !description || !content) if (!title || !description || !content)
throw createError({ throw createError({
+6 -8
View File
@@ -6,23 +6,21 @@ export async function handleFileUpload(
h3: H3Event<EventHandlerRequest>, h3: H3Event<EventHandlerRequest>,
metadata: { [key: string]: string }, metadata: { [key: string]: string },
permissions: Array<string>, permissions: Array<string>,
): Promise< max = -1,
[string | undefined, { [key: string]: string }, Pull, Dump] | undefined ): Promise<[string[], { [key: string]: string }, Pull, Dump] | undefined> {
> {
const formData = await readMultipartFormData(h3); const formData = await readMultipartFormData(h3);
if (!formData) return undefined; if (!formData) return undefined;
const transactionalHandler = new ObjectTransactionalHandler(); const transactionalHandler = new ObjectTransactionalHandler();
const [add, pull, dump] = transactionalHandler.new(metadata, permissions); const [add, pull, dump] = transactionalHandler.new(metadata, permissions);
const options: { [key: string]: string } = {}; const options: { [key: string]: string } = {};
let id; const ids = [];
for (const entry of formData) { for (const entry of formData) {
if (entry.filename) { if (entry.filename) {
// Only pick one file if (max > 0 && ids.length >= max) continue;
if (id) continue;
// Add file to transaction handler so we can void it later if we error out // 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; continue;
} }
if (!entry.name) continue; if (!entry.name) continue;
@@ -30,5 +28,5 @@ export async function handleFileUpload(
options[entry.name] = entry.data.toString("utf-8"); options[entry.name] = entry.data.toString("utf-8");
} }
return [id, options, pull, dump]; return [ids, options, pull, dump];
} }