feat: multi s3 arg input (#6187)

* feat: multi s3 arg input

* nits
This commit is contained in:
HugoCasa
2025-07-15 15:42:25 +00:00
committed by GitHub
parent d3a05d495d
commit 9053a931ce
4 changed files with 285 additions and 161 deletions
+18 -78
View File
@@ -9,7 +9,7 @@
emptyString,
getSchemaFromProperties
} from '$lib/utils'
import { DollarSign, Pipette, Plus, X, Check, Loader2 } from 'lucide-svelte'
import { DollarSign, Plus, X, Check, Loader2 } from 'lucide-svelte'
import { createEventDispatcher, onDestroy, onMount, tick, untrack } from 'svelte'
import { fade } from 'svelte/transition'
import { Button, SecondsInput } from './common'
@@ -25,7 +25,6 @@
import ArgEnum from './ArgEnum.svelte'
import DateTimeInput from './DateTimeInput.svelte'
import DateInput from './DateInput.svelte'
import S3FilePicker from './S3FilePicker.svelte'
import CurrencyInput from './apps/components/inputs/currency/CurrencyInput.svelte'
import FileUpload from './common/fileUpload/FileUpload.svelte'
import autosize from '$lib/autosize'
@@ -42,6 +41,7 @@
import type { ComponentCustomCSS } from './apps/types'
import MultiSelect from './select/MultiSelect.svelte'
import { safeSelectItems } from './select/utils.svelte'
import S3ArgInput from './common/fileUpload/S3ArgInput.svelte'
interface Props {
label?: string
@@ -227,8 +227,6 @@
let ignoreValueUndefined = $state(false)
let error: string = $state('')
let s3FilePicker: S3FilePicker | undefined = $state()
let s3FileUploadRawMode: boolean = $state(false)
let isListJson = $state(false)
let hasIsListJsonChanged = $state(false)
@@ -499,16 +497,6 @@
})
</script>
<S3FilePicker
bind:this={s3FilePicker}
bind:selectedFileKey={value}
on:close={() => {
rawValue = JSON.stringify(value, null, 2)
editor?.setCode(rawValue)
}}
readOnlyMode={false}
/>
<!-- svelte-ignore a11y_autofocus -->
<div
class={twMerge(
@@ -631,6 +619,22 @@
<span>&nbsp; Not set</span>
{/if}
</div>
{:else if (inputCat == 'resource-object' && format && format.split('-').length > 1 && format
.replace('resource-', '')
.replace('_', '')
.toLowerCase() == 's3object') || (inputCat == 'list' && itemsType?.resourceType === 's3_object')}
<S3ArgInput
multiple={inputCat == 'list'}
bind:value
{defaultValue}
{setNewValueFromCode}
{workspace}
onFocus={() => dispatch('focus')}
onBlur={() => dispatch('blur')}
bind:editor
{appPath}
{computeS3ForceViewerPolicies}
/>
{:else if inputCat == 'list' && !isListJson}
<div class="w-full flex gap-4">
<div class="w-full">
@@ -851,70 +855,6 @@
}}
{showSchemaExplorer}
/>
{:else if inputCat == 'resource-object' && format && format.split('-').length > 1 && format
.replace('resource-', '')
.replace('_', '')
.toLowerCase() == 's3object'}
<div class="flex flex-col w-full gap-1">
<Toggle
class="flex justify-end"
bind:checked={s3FileUploadRawMode}
size="xs"
options={{ left: 'Raw S3 object input' }}
/>
{#if s3FileUploadRawMode}
{#await import('$lib/components/JsonEditor.svelte')}
<Loader2 class="animate-spin" />
{:then Module}
<Module.default
bind:editor
on:focus={(e) => {
dispatch('focus')
}}
on:blur={(e) => {
dispatch('blur')
}}
code={JSON.stringify(value ?? defaultValue ?? { s3: '' }, null, 2)}
on:changeValue={(e) => {
setNewValueFromCode(e.detail)
}}
/>
{/await}
{:else}
<FileUpload
{appPath}
computeForceViewerPolicies={computeS3ForceViewerPolicies}
{workspace}
allowMultiple={false}
randomFileKey={true}
on:addition={(evt) => {
value = {
s3: evt.detail?.path ?? '',
filename: evt.detail?.filename ?? ''
}
}}
on:deletion={(evt) => {
value = {
s3: ''
}
}}
defaultValue={defaultValue?.s3}
initialValue={value}
/>
{/if}
<Button
variant="border"
color="light"
size="xs"
btnClasses="mt-1"
on:click={() => {
s3FilePicker?.open?.(value)
}}
startIcon={{ icon: Pipette }}
>
Choose an object from the catalog
</Button>
</div>
{:else if inputCat == 'object' || inputCat == 'resource-object' || isListJson}
{#if oneOf && oneOf.length >= 2}
<div class="flex flex-col gap-2 w-full">
@@ -7,7 +7,7 @@
import { workspaceStore } from '$lib/stores'
import { AppService, HelpersService } from '$lib/gen'
import { writable, type Writable } from 'svelte/store'
import { Ban, CheckCheck, FileWarning, Files, RefreshCcw, Trash } from 'lucide-svelte'
import { Ban, CheckCheck, FileWarning, Files, RefreshCcw, Trash, XIcon } from 'lucide-svelte'
import { twMerge } from 'tailwind-merge'
import { createEventDispatcher, onDestroy } from 'svelte'
import { emptyString } from '$lib/utils'
@@ -94,16 +94,15 @@
)
init()
function transform(initialValue: { s3: string; filename?: string }) {
return {
name: initialValue?.filename ?? initialValue?.s3 ?? 'Unknown file',
path: initialValue?.s3,
fromBucket: true
}
}
function init() {
if (initialS3) {
function transform(initialValue: { s3: string; filename: string }) {
return {
name: initialValue.filename,
size: 0,
progress: 100,
path: initialValue?.s3
}
}
for (const s3 of initialS3) {
if (!$fileUploads.find((fileUpload) => fileUpload.path === s3)) {
let initialFileUploads = initialValue
@@ -117,15 +116,24 @@
}
}
export function addUpload(upload: { s3: string; filename?: string }) {
$fileUploads = [...$fileUploads, transform(upload)]
}
export function setUpload(upload: { s3: string; filename?: string }) {
$fileUploads = [transform(upload)]
}
type FileUploadData = {
name: string
size: number
progress: number
size?: number
progress?: number
cancelled?: boolean
errorMessage?: string
path?: string
file?: File
deleteToken?: string
fromBucket?: boolean
}
async function handleChange(files: File[] | undefined) {
@@ -394,8 +402,28 @@
})
</script>
{#snippet fileInput()}
<FileInput
{folderOnly}
{disabled}
accept={acceptedFileTypes?.join(',')}
multiple={allowMultiple}
returnFileNames
{iconSize}
on:change={({ detail }) => {
forceDisplayUploads = false
handleChange(detail)
}}
class={twMerge('w-full h-full', customClass, 'wm-file-input')}
style={customStyle}
defaultFile={defaultValue}
>
{containerText}{#if disabled}<br />(Disabled){/if}
</FileInput>
{/snippet}
<div class="w-full h-full p-0 flex flex-col">
{#if $fileUploads.length > 0 && !forceDisplayUploads}
{#if $fileUploads.length > 0}
<div class="border rounded-md flex flex-col gap-1 divide-y h-full w-full p-1">
<div class="flex h-full overflow-y-auto flex-col">
{#each $fileUploads as fileUpload}
@@ -468,7 +496,7 @@
Remove from list
</Button>
{/if}
{#if fileUpload.progress < 100 && !fileUpload.cancelled && !fileUpload.errorMessage}
{#if fileUpload.progress !== undefined && fileUpload.progress < 100 && !fileUpload.cancelled && !fileUpload.errorMessage}
<Button
size="xs2"
color="light"
@@ -506,100 +534,115 @@
>
Delete
</Button>
{:else if fileUpload.fromBucket}
<Button
size="xs2"
color="red"
variant="border"
on:click={() => {
$fileUploads = $fileUploads.filter(
(_fileUpload) => _fileUpload.name !== fileUpload.name
)
if (fileUpload.path) {
dispatch('deletion', { path: fileUpload.path })
}
}}
startIcon={{
icon: XIcon
}}
>
Remove
</Button>
{/if}
</div>
</div>
<FileProgressBar
progress={fileUpload.progress}
color={fileUpload.errorMessage
? '#ef4444'
: fileUpload.cancelled
? '#eab308'
: fileUpload.progress === 100
? '#22c55e'
: '#3b82f6'}
ended={fileUpload.cancelled || fileUpload.errorMessage !== undefined}
>
{#if fileUpload.errorMessage}
<span class="text-xs text-red-600">{fileUpload.errorMessage}</span>
{:else if fileUpload.cancelled}
<span class="text-xs text-yellow-600">Upload cancelled</span>
{/if}
</FileProgressBar>
{#if !fileUpload.fromBucket}
<FileProgressBar
progress={fileUpload.progress}
color={fileUpload.errorMessage
? '#ef4444'
: fileUpload.cancelled
? '#eab308'
: fileUpload.progress === 100
? '#22c55e'
: '#3b82f6'}
ended={fileUpload.cancelled || fileUpload.errorMessage !== undefined}
>
{#if fileUpload.errorMessage}
<span class="text-xs text-red-600">{fileUpload.errorMessage}</span>
{:else if fileUpload.cancelled}
<span class="text-xs text-yellow-600">Upload cancelled</span>
{/if}
</FileProgressBar>
{/if}
{#if !(fileUpload.cancelled || fileUpload.errorMessage !== undefined)}
<span class="text-xs text-gray-500 dark:text-gray-200">
{fileUpload.progress === 100 ? 'Upload finished' : `Uploading`} to path: {fileUpload.path ??
'N/A'}
{#if fileUpload.fromBucket}
{fileUpload.path ?? 'N/A'}
{:else}
{fileUpload.progress === 100 ? 'Upload finished' : `Uploading`} to path: {fileUpload.path ??
'N/A'}
{/if}
</span>
{/if}
</div>
{/each}
</div>
{#if allowMultiple}
<div class="flex flex-row gap-1 items-center justify-end p-1">
{#if !$fileUploads.every((fileUpload) => fileUpload.progress === 100 || fileUpload.cancelled)}
<Button
size="xs2"
color="light"
on:click={() => {
$fileUploads = $fileUploads.map((fileUpload) => {
if (fileUpload.progress === 100 || fileUpload.cancelled) {
return fileUpload
}
fileUpload.cancelled = true
fileUpload.progress = 0
return fileUpload
})
}}
startIcon={{
icon: Ban
}}
>
Cancel all uploads
</Button>
{/if}
<div class="flex flex-row gap-1 items-center justify-end p-1">
{#if !forceDisplayUploads && (allowMultiple || folderOnly) && !$fileUploads.every((fileUpload) => fileUpload.progress === 100 || fileUpload.cancelled || fileUpload.fromBucket)}
<Button
size="xs2"
color="light"
on:click={() => {
forceDisplayUploads = true
$fileUploads = $fileUploads.map((fileUpload) => {
if (fileUpload.progress === 100 || fileUpload.cancelled) {
return fileUpload
}
fileUpload.cancelled = true
fileUpload.progress = 0
return fileUpload
})
}}
startIcon={{
icon: Files
icon: Ban
}}
disabled={$fileUploads.some(
(fileUpload) => fileUpload.progress !== 100 && !fileUpload.cancelled
)}
>
Upload more files
Cancel all uploads
</Button>
</div>
{/if}
{/if}
{#if allowMultiple}
{#if forceDisplayUploads}
{@render fileInput()}
{:else}
<Button
size="xs2"
color="light"
on:click={() => {
forceDisplayUploads = true
}}
startIcon={{
icon: Files
}}
disabled={$fileUploads.some(
(fileUpload) =>
fileUpload.progress !== 100 && !fileUpload.cancelled && !fileUpload.fromBucket
)}
>
Upload more files
</Button>
{/if}
{/if}
</div>
</div>
{:else}
<FileInput
{folderOnly}
{disabled}
accept={acceptedFileTypes?.join(',')}
multiple={allowMultiple}
returnFileNames
{iconSize}
on:change={({ detail }) => {
forceDisplayUploads = false
handleChange(detail)
}}
class={twMerge('w-full h-full', customClass, 'wm-file-input')}
style={customStyle}
defaultFile={defaultValue}
>
{containerText}{#if disabled}<br />(Disabled){/if}
</FileInput>
{@render fileInput()}
{/if}
{#if initialS3 && initialS3.length > 0 && $fileUploads.length == 0}
<div class="flex flex-row gap-1 items-center p-1">
<span class="text-sm">
File currently selected: {initialS3?.join(', ')}
File{initialS3.length > 1 ? 's' : ''} currently selected: {initialS3?.join(', ')}
</span>
</div>
{/if}
@@ -0,0 +1,142 @@
<script lang="ts">
import S3FilePicker from '$lib/components/S3FilePicker.svelte'
import Toggle from '$lib/components/Toggle.svelte'
import { Loader2, Pipette } from 'lucide-svelte'
import FileUpload from './FileUpload.svelte'
import type SimpleEditor from '$lib/components/SimpleEditor.svelte'
import Button from '../button/Button.svelte'
let {
multiple,
value = $bindable(),
defaultValue,
setNewValueFromCode,
onFocus,
onBlur,
computeS3ForceViewerPolicies,
workspace,
editor = $bindable(),
appPath
}: {
multiple: boolean
value: any
defaultValue: any
setNewValueFromCode: (v: any) => void
onFocus: () => void
onBlur: () => void
computeS3ForceViewerPolicies:
| (() =>
| {
allowed_resources: string[]
allow_user_resources: boolean
allow_workspace_resource: boolean
file_key_regex: string
}
| undefined)
| undefined
workspace: string | undefined
editor: SimpleEditor | undefined
appPath: string | undefined
} = $props()
let s3FileUploadRawMode: boolean = $state(false)
let s3FilePicker: S3FilePicker | undefined = $state(undefined)
let fileUpload: FileUpload | undefined = $state(undefined)
</script>
<S3FilePicker
bind:this={s3FilePicker}
on:selectAndClose={(ev) => {
if (multiple) {
if (Array.isArray(value)) {
value.push(ev.detail)
} else {
value = [ev.detail]
}
fileUpload?.addUpload(ev.detail)
} else {
value = ev.detail
fileUpload?.setUpload(value)
}
editor?.setCode(JSON.stringify(value))
}}
readOnlyMode={false}
/>
<div class="flex flex-col w-full gap-1">
<Toggle
class="flex justify-end"
bind:checked={s3FileUploadRawMode}
size="xs"
options={{ left: `Raw S3 object${multiple ? 's' : ''} input` }}
/>
{#if s3FileUploadRawMode}
{#await import('$lib/components/JsonEditor.svelte')}
<Loader2 class="animate-spin" />
{:then Module}
<Module.default
bind:editor
on:focus={(e) => {
onFocus()
}}
on:blur={(e) => {
onBlur()
}}
code={JSON.stringify(value ?? defaultValue ?? (multiple ? [] : { s3: '' }), null, 2)}
on:changeValue={(e) => {
setNewValueFromCode(e.detail)
}}
/>
{/await}
{:else}
<FileUpload
bind:this={fileUpload}
{appPath}
computeForceViewerPolicies={computeS3ForceViewerPolicies}
{workspace}
allowMultiple={multiple}
randomFileKey={true}
on:addition={(evt) => {
const s3Object = {
s3: evt.detail?.path ?? '',
filename: evt.detail?.filename ?? ''
}
if (multiple) {
if (Array.isArray(value)) {
value.push(s3Object)
} else {
value = [s3Object]
}
} else {
value = s3Object
}
}}
on:deletion={(evt) => {
if (multiple) {
if (Array.isArray(value)) {
value = value.filter((v) => v.s3 !== evt.detail?.path)
}
} else {
value = {
s3: ''
}
}
}}
defaultValue={defaultValue?.s3}
initialValue={value}
/>
{/if}
<Button
variant="border"
color="light"
size="xs"
btnClasses="mt-1"
on:click={() => {
s3FilePicker?.open?.(value)
}}
startIcon={{ icon: Pipette }}
>
{multiple ? 'Add' : 'Choose'} an object from the catalog
</Button>
</div>
@@ -502,7 +502,6 @@
disabled={!can_write}
/>
{/if}
{s3FileUploadRawMode}
{#if s3FileUploadRawMode}
{#if can_write}
<JsonEditor