diff --git a/src/components/BucketView.vue b/src/components/BucketView.vue index ba226f289d7650b90af4539a1b1184b73adf2a35..72211963814354bad2a02d41ec129c080d3ed218 100644 --- a/src/components/BucketView.vue +++ b/src/components/BucketView.vue @@ -10,6 +10,7 @@ import { Tooltip } from "bootstrap"; import PermissionListModal from "@/components/Modals/PermissionListModal.vue"; import UploadObjectModal from "@/components/Modals/UploadObjectModal.vue"; import PermissionModal from "@/components/Modals/PermissionModal.vue"; +import CreateFolderModal from "@/components/Modals/CreateFolderModal.vue"; import { S3Client } from "@aws-sdk/client-s3"; import { awsAuthMiddlewareOptions } from "@aws-sdk/middleware-signing"; @@ -203,7 +204,7 @@ const visibleObjects: ComputedRef<(S3ObjectWithFolder | S3PseudoFolder)[]> = } as S3PseudoFolder; }) ); - return arr; + return arr.filter((obj) => !obj.key.endsWith(".s3keep")); }); const subFolderInUrl: ComputedRef<boolean> = computed( @@ -390,7 +391,6 @@ watch( </button> <upload-object-modal :bucket-name="props.bucketName" - :sub-folders="[]" :s3-client="client" modalID="upload-object-modal" modal-label="some-label" @@ -402,13 +402,22 @@ watch( type="button" class="btn btn-secondary m-2 tooltip-container" :disabled="errorLoadingObjects" - data-bs-toggle="tooltip" + data-bs-toggle="modal" data-bs-title="Create Folder" + data-bs-target="#create-folder-modal" > <bootstrap-icon icon="plus-lg" :width="16" :height="16" fill="white" /> Folder <span class="visually-hidden">Add Folder</span> </button> + <create-folder-modal + :bucket-name="props.bucketName" + :s3-client="client" + modalID="create-folder-modal" + modal-label="some-label" + :key-prefix="currentSubFolders.join('/')" + @folder-created="objectUploaded" + /> <!-- Add bucket permission button --> <button :hidden="props.permission != null" diff --git a/src/components/Modals/CreateFolderModal.vue b/src/components/Modals/CreateFolderModal.vue new file mode 100644 index 0000000000000000000000000000000000000000..d789c8f9cca8013154acf82e90fc4bea7ec8035c --- /dev/null +++ b/src/components/Modals/CreateFolderModal.vue @@ -0,0 +1,194 @@ +<script setup lang="ts"> +import type { S3Client } from "@aws-sdk/client-s3"; +import { PutObjectCommand } from "@aws-sdk/client-s3"; +import BootstrapModal from "@/components/Modals/BootstrapModal.vue"; +import { computed, onMounted, reactive } from "vue"; +import type { ComputedRef } from "vue"; +import type { S3ObjectMetaInformation } from "@/client"; +import dayjs from "dayjs"; +import { Modal, Toast } from "bootstrap"; + +const props = defineProps<{ + modalID: string; + modalLabel: string; + bucketName: string; + keyPrefix: string; + s3Client: S3Client; +}>(); + +const randomIDSuffix = Math.random().toString(16).substr(2, 8); +let uploadModal: Modal | null = null; +let successToast: Toast | null = null; +let errorToast: Toast | null = null; + +const currentFolders: ComputedRef<string[]> = computed(() => + props.keyPrefix.split("/") +); + +const emit = defineEmits<{ + (e: "folder-created", object: S3ObjectMetaInformation): void; +}>(); + +const formState = reactive({ + folderName: "", + uploading: false, +} as { + folderName: string; + uploading: boolean; +}); + +function uploadFolder() { + const key = + (props.keyPrefix.length > 0 + ? props.keyPrefix + "/" + formState.folderName + : formState.folderName) + "/.s3keep"; + const command = new PutObjectCommand({ + Bucket: props.bucketName, + Body: "", + ContentType: "text/plain", + Key: key, + }); + formState.uploading = true; + props.s3Client + .send(command) + .then(() => { + uploadModal?.hide(); + successToast?.show(); + emit("folder-created", { + key: key, + bucket: props.bucketName, + size: 0, + last_modified: dayjs().toISOString(), + }); + formState.folderName = ""; + }) + .catch((e) => { + console.error(e); + errorToast?.show(); + }) + .finally(() => { + formState.uploading = false; + }); +} + +onMounted(() => { + uploadModal = new Modal("#" + props.modalID); + successToast = new Toast("#successToast-" + randomIDSuffix); + errorToast = new Toast("#errorToast-" + randomIDSuffix); +}); +</script> + +<template> + <div class="toast-container position-fixed top-0 end-0 p-3"> + <div + role="alert" + aria-live="assertive" + aria-atomic="true" + class="toast text-bg-success align-items-center border-0" + data-bs-autohide="true" + :id="'successToast-' + randomIDSuffix" + > + <div class="d-flex"> + <div class="toast-body">Successfully created Folder</div> + <button + type="button" + class="btn-close btn-close-white me-2 m-auto" + data-bs-dismiss="toast" + aria-label="Close" + ></button> + </div> + </div> + </div> + <div class="toast-container position-fixed top-0 end-0 p-3"> + <div + role="alert" + aria-live="assertive" + aria-atomic="true" + class="toast text-bg-danger align-items-center border-0" + data-bs-autohide="true" + :id="'errorToast-' + randomIDSuffix" + > + <div class="d-flex"> + <div class="toast-body"> + There has been some Error.<br /> + Try again later + </div> + <button + type="button" + class="btn-close btn-close-white me-2 m-auto" + data-bs-dismiss="toast" + aria-label="Close" + ></button> + </div> + </div> + </div> + <bootstrap-modal + :modalID="modalID" + :static-backdrop="true" + :modal-label="modalLabel" + > + <template v-slot:header> + <h4>Create folder in</h4> + <ol class="breadcrumb"> + <li class="breadcrumb-item">{{ props.bucketName }}</li> + <li + class="breadcrumb-item" + v-for="folder in currentFolders" + :key="folder" + > + {{ folder }} + </li> + </ol> + </template> + <template v-slot:body> + <div class="container-fluid"> + <div class="row"> + <form + class="col-7" + :id="'uploadFolderForm' + randomIDSuffix" + @submit.prevent="uploadFolder" + > + <div class="mb-3"> + <label :for="'folderName' + randomIDSuffix" class="form-label" + >Folder Name</label + > + <input + type="text" + class="form-control" + :id="'folderName' + randomIDSuffix" + required + v-model="formState.folderName" + /> + </div> + </form> + <div class="col-5"> + Note: Delimiters ('/') are allowed in the folder name to place the + new folder into a folder that will be created when the folder is + created (to any depth of folders). + </div> + </div> + </div> + </template> + <template v-slot:footer> + <button type="button" class="btn btn-secondary" data-bs-dismiss="modal"> + Close + </button> + <button + :disabled="formState.uploading" + type="submit" + :form="'uploadFolderForm' + randomIDSuffix" + class="btn btn-primary" + > + <span + v-if="formState.uploading" + class="spinner-border spinner-border-sm" + role="status" + aria-hidden="true" + ></span> + Create + </button> + </template> + </bootstrap-modal> +</template> + +<style scoped></style> diff --git a/vite.config.ts b/vite.config.ts index 1efdea2b75099a671e1290beda4addab8689c3b4..5679c45caa3c4a796331cd1a26a97a29d4eea82f 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -27,10 +27,8 @@ export default defineConfig({ // Enable esbuild polyfill plugins plugins: [ NodeGlobalsPolyfillPlugin({ - stream: true, buffer: true, process: true, - util: true, }), NodeModulesPolyfillPlugin(), ],