Skip to content

Commit

Permalink
feat: file upload input (#6487)
Browse files Browse the repository at this point in the history
* Basic FileInput

* Add error state

* Add drag-drop support

* Add drop-over state and loading state

* Fix lint

* Add preview
  • Loading branch information
AdityaHegde authored Feb 3, 2025
1 parent 8a22a74 commit d61e3f8
Show file tree
Hide file tree
Showing 2 changed files with 191 additions and 0 deletions.
172 changes: 172 additions & 0 deletions web-common/src/components/forms/FileInput.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
<script lang="ts">
import LoadingSpinner from "@rilldata/web-common/components/icons/LoadingSpinner.svelte";
import Viz from "@rilldata/web-common/components/icons/Viz.svelte";
export let value: string | string[] | undefined = undefined;
export let error: string | Record<string | number, string[]> | undefined =
undefined;
export let multiple: boolean = false;
export let accept: string | undefined = undefined;
// Currently we upload either to runtime or cloud.
// Implementation of it will be upto the caller of this component.
export let uploadFile: (file: File) => Promise<string>;
$: values = value ? (multiple ? (value as string[]) : [value as string]) : [];
$: errors = error ? (multiple ? error : { 0: error }) : [];
$: uploading = {};
$: uploadErrors = {};
let fileInput: HTMLInputElement;
// maintain a list of filenames to show in error messages.
// since the final uploaded url set in `value` is usually not the same this is needed.
let fileNames: string[] = value
? multiple
? (value as string[])
: [value as string]
: [];
$: hasValue = values.length > 0 || Object.values(uploading).some((u) => u);
function uploadFiles(files: FileList) {
uploading = {};
uploadErrors = {};
if (multiple) {
value = new Array(files.length).fill("");
fileNames = new Array<string>(files.length).fill("");
} else {
value = "";
fileNames = [""];
}
for (let i = 0; i < files.length; i++) {
void uploadFileWrapper(files[i], i);
}
}
async function uploadFileWrapper(file: File, i: number) {
uploading[i] = true;
try {
fileNames[i] = file.name;
const url = await uploadFile(file);
if (multiple) {
if (value === undefined) {
value = [];
}
(value as string[])[i] = url;
} else {
value = url;
}
} catch (err) {
uploadErrors[i] = err.message;
}
uploading[i] = false;
}
function handleInput() {
if (!fileInput.files) return;
uploadFiles(fileInput.files);
}
function handleFileDrop(event: DragEvent) {
dragOver = false;
if (!event.dataTransfer?.files?.length) return;
uploadFiles(event.dataTransfer.files);
}
let dragOver = false;
$: errorMessages = Object.values({
...(errors as Record<string, any>),
...uploadErrors,
})
.map((e, i) => (fileNames[i] && e ? `${fileNames[i]}:${e}` : ""))
.filter(Boolean);
</script>

<div class="container grid">
<button
class="upload-button"
on:click={() => fileInput.click()}
on:dragenter|preventDefault|stopPropagation={() => (dragOver = true)}
on:dragleave|preventDefault|stopPropagation={() => (dragOver = false)}
on:dragover|preventDefault|stopPropagation
on:drop|preventDefault={handleFileDrop}
class:bg-neutral-100={!dragOver}
class:bg-primary-100={dragOver}
>
{#if hasValue}
<div class="upload-preview">
{#each fileNames as _, i (i)}
{@const isUploading = !!uploading[i]}
{@const hasError =
(!!uploadErrors[i] || !!errors?.[i]) && !isUploading}
{@const val = values[i]}
{#if (val || isUploading) && !hasError}
<div class="border border-neutral-400 p-1">
{#if isUploading}
<LoadingSpinner size="36px" />
{:else}
<img src={val} alt="upload" class="h-10 w-fit" />
{/if}
</div>
{/if}
{/each}
</div>
{:else}
<Viz size="28px" class="text-gray-400 pointer-events-none" />
<div class="container-flex-col pointer-events-none">
<span class="upload-title"> Upload an image </span>
{#if multiple}
<span class="upload-description">
Support for a single or bulk upload.
</span>
{/if}
</div>
{/if}
</button>
{#if errorMessages.length > 0}
<div class="error">
{#each errorMessages as errorMessage, i (i)}
<div>{errorMessage}</div>
{/each}
</div>
{/if}
<input
type="file"
{accept}
hidden
{multiple}
bind:this={fileInput}
on:input={handleInput}
/>
</div>

<style lang="postcss">
.container {
@apply flex flex-col gap-y-2;
}
.container-flex-col {
@apply flex flex-col;
}
.upload-button {
@apply flex flex-row gap-x-2.5 items-center justify-center py-5 min-h-10 w-80;
@apply border border-neutral-400;
}
.upload-title {
@apply text-sm font-medium text-left text-gray-500;
}
.upload-description {
@apply text-xs font-normal text-gray-400;
}
.upload-preview {
@apply flex flex-wrap w-full gap-x-1 items-center justify-center pointer-events-none px-4;
}
.error {
@apply text-red-600 text-xs py-px mt-0.5;
}
</style>
19 changes: 19 additions & 0 deletions web-common/src/components/icons/Attachment.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<script>
export let size = "1em";
export let color = "currentColor";
export let className = "";
</script>

<svg
height={size}
viewBox="0 0 14 14"
fill={color}
xmlns="http://www.w3.org/2000/svg"
class={className}
>
<path
d="M11.1766 2.07461C9.7047 0.602734 7.30783 0.602734 5.83751 2.07461L1.75939 6.14961C1.73283 6.17617 1.71876 6.21211 1.71876 6.24961C1.71876 6.28711 1.73283 6.32305 1.75939 6.34961L2.33595 6.92617C2.36231 6.95241 2.39798 6.96714 2.43517 6.96714C2.47236 6.96714 2.50804 6.95241 2.53439 6.92617L6.61251 2.85117C7.11877 2.34492 7.7922 2.0668 8.50783 2.0668C9.22345 2.0668 9.89689 2.34492 10.4016 2.85117C10.9078 3.35742 11.186 4.03086 11.186 4.74492C11.186 5.46055 10.9078 6.13242 10.4016 6.63867L6.24533 10.7934L5.57189 11.4668C4.9422 12.0965 3.91876 12.0965 3.28908 11.4668C2.98439 11.1621 2.8172 10.7574 2.8172 10.3262C2.8172 9.89492 2.98439 9.49024 3.28908 9.18555L7.41252 5.06367C7.5172 4.96055 7.6547 4.90273 7.80158 4.90273H7.80314C7.95002 4.90273 8.08595 4.96055 8.18908 5.06367C8.29376 5.16836 8.35002 5.30586 8.35002 5.45273C8.35002 5.59805 8.2922 5.73555 8.18908 5.83867L4.81876 9.20586C4.7922 9.23242 4.77814 9.26836 4.77814 9.30586C4.77814 9.34336 4.7922 9.3793 4.81876 9.40586L5.39533 9.98242C5.42168 10.0087 5.45736 10.0234 5.49455 10.0234C5.53174 10.0234 5.56741 10.0087 5.59377 9.98242L8.96251 6.61367C9.27345 6.30273 9.44377 5.89023 9.44377 5.45117C9.44377 5.01211 9.27189 4.59805 8.96251 4.28867C8.32033 3.64648 7.27658 3.64805 6.63439 4.28867L6.23439 4.69023L2.51251 8.41055C2.25991 8.66167 2.05967 8.96045 1.92341 9.28956C1.78716 9.61866 1.7176 9.97154 1.71876 10.3277C1.71876 11.0512 2.00158 11.7309 2.51251 12.2418C3.0422 12.7699 3.73595 13.034 4.4297 13.034C5.12345 13.034 5.8172 12.7699 6.34533 12.2418L11.1766 7.41367C11.8875 6.70117 12.2813 5.75273 12.2813 4.74492C12.2828 3.73555 11.8891 2.78711 11.1766 2.07461Z"
fill="black"
fill-opacity="0.45"
/>
</svg>

1 comment on commit d61e3f8

@github-actions
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.