diff --git a/src/classes/IOPromise.ts b/src/classes/IOPromise.ts index 3406694..f94bb8d 100644 --- a/src/classes/IOPromise.ts +++ b/src/classes/IOPromise.ts @@ -278,7 +278,9 @@ export class MultipleableIOPromise< MethodName extends T_IO_MULTIPLEABLE_METHOD_NAMES, Props extends T_IO_PROPS = T_IO_PROPS, Output = ComponentReturnValue, - DefaultValue = Output + DefaultValue = T_IO_PROPS extends { defaultValue?: any } + ? Output + : never > extends InputIOPromise { defaultValueGetter: | ((defaultValue: DefaultValue) => T_IO_RETURNS) @@ -305,16 +307,20 @@ export class MultipleableIOPromise< this.defaultValueGetter = defaultValueGetter } - multiple({ defaultValue }: { defaultValue?: DefaultValue[] } = {}) { + multiple({ + defaultValue, + }: { + defaultValue?: DefaultValue[] + } = {}): MultipleIOPromise { let transformedDefaultValue: T_IO_RETURNS[] | undefined - if (defaultValue) { + const propsSchema = ioSchema[this.methodName].props + if (defaultValue && 'defaultValue' in propsSchema.shape) { const { defaultValueGetter } = this const potentialDefaultValue = defaultValueGetter ? defaultValue.map(dv => defaultValueGetter(dv)) : (defaultValue as unknown as T_IO_RETURNS[]) try { - const propsSchema = ioSchema[this.methodName].props const defaultValueSchema = propsSchema.shape.defaultValue transformedDefaultValue = z .array(defaultValueSchema.unwrap()) diff --git a/src/components/upload.ts b/src/components/upload.ts index 0e3e337..ea675b2 100644 --- a/src/components/upload.ts +++ b/src/components/upload.ts @@ -24,7 +24,7 @@ async function retryFetch(url: string): Promise { type UploaderProps = T_IO_PROPS<'UPLOAD_FILE'> & { generatePresignedUrls?: ( - state: T_IO_STATE<'UPLOAD_FILE'> + state: NonNullable['files']>[0] ) => Promise<{ uploadUrl: string; downloadUrl: string }> } @@ -34,8 +34,7 @@ export function file(logger: Logger) { return { props: { ...props, - uploadUrl: isProvidingUrls ? null : undefined, - downloadUrl: isProvidingUrls ? null : undefined, + fileUrls: isProvidingUrls ? null : undefined, }, getValue({ url, ...response }: T_IO_RETURNS<'UPLOAD_FILE'>) { return { @@ -61,20 +60,14 @@ export function file(logger: Logger) { } }, async onStateChange(newState: T_IO_STATE<'UPLOAD_FILE'>) { - if (!generatePresignedUrls) { - return { uploadUrl: undefined, downloadUrl: undefined } + if (!generatePresignedUrls || !newState.files) { + return { fileUrls: undefined } } - try { - const urls = await generatePresignedUrls(newState) - return urls - } catch (error) { - // TODO: We should not swallow this error after merging #1012 - logger.error( - 'An error was unexpectedly thrown from the `generatePresignedUrls` function:' - ) - logger.error(error) - return { uploadUrl: 'error', downloadUrl: 'error' } + return { + fileUrls: await Promise.all( + newState.files.map(fileState => generatePresignedUrls(fileState)) + ), } }, } diff --git a/src/examples/basic/index.ts b/src/examples/basic/index.ts index 3ddddd1..62e44eb 100644 --- a/src/examples/basic/index.ts +++ b/src/examples/basic/index.ts @@ -1101,7 +1101,7 @@ const interval = new Interval({ # What to expect from here on out _This has been adapted from the [Tailwind](https://tailwindcss.com) typography plugin demo._ - + What follows from here is just a bunch of absolute nonsense I've written to demo typography. It includes every sensible typographic element I could think of, like **bold text**, unordered lists, ordered lists, code blocks, block quotes, and _even italics_. It's important to cover all of these use cases for a few reasons: @@ -1109,7 +1109,7 @@ const interval = new Interval({ 1. We want everything to look good out of the box. 2. Really just the first reason, that's the whole point of the plugin. 3. Here's a third pretend reason though a list with three items looks more realistic than a list with two items. - + Now we're going to try out another header style. ## Typography should be easy @@ -1128,7 +1128,7 @@ const interval = new Interval({ - And this is the last item in the list. ### What does code look like? - + Code blocks should look okay by default, although most people will probably want to use \`io.display.code\`: \`\`\` @@ -1139,7 +1139,7 @@ const interval = new Interval({ } }) \`\`\` - + #### And finally, an H4 And that's the end of this demo. @@ -1311,49 +1311,75 @@ const interval = new Interval({ return { message: 'OK, notified!' } }, - upload: async (io, ctx) => { - const customDestinationFile = await io.input.file('Upload an image!', { - helpText: 'Will be uploaded to the custom destination.', - allowedExtensions: ['.gif', '.jpg', '.jpeg', '.png'], - generatePresignedUrls: async ({ name }) => { - const urlSafeName = name.replace(/ /g, '-') - const path = `custom-endpoint/${new Date().getTime()}-${urlSafeName}` - - return generateS3Urls(path) - }, - }) - - console.log(await customDestinationFile.url()) + uploads: new Page({ + name: 'Uploads', + routes: { + custom_destination: async io => { + const customDestinationFile = await io.input.file( + 'Upload an image!', + { + helpText: 'Will be uploaded to the custom destination.', + allowedExtensions: ['.gif', '.jpg', '.jpeg', '.png'], + generatePresignedUrls: async ({ name }) => { + const urlSafeName = name.replace(/ /g, '-') + const path = `custom-endpoint/${new Date().getTime()}-${urlSafeName}` - const file = await io.input.file('Upload an image!', { - helpText: - 'Will be uploaded to Interval and expire after the action finishes running.', - allowedExtensions: ['.gif', '.jpg', '.jpeg', '.png'], - }) + return generateS3Urls(path) + }, + } + ) - console.log(file) + console.log(await customDestinationFile.url()) - const { text, json, buffer, url, ...rest } = file + const { text, json, buffer, url, ...rest } = customDestinationFile - return { - ...rest, - url: await url(), - text: rest.type.includes('text/') - ? await text().catch(err => { - console.log('Invalid text', err) - return undefined + return { + ...rest, + url: await url(), + text: rest.type.includes('text/') + ? await text().catch(err => { + console.log('Invalid text', err) + return undefined + }) + : undefined, + json: rest.type.includes('text/') + ? await json() + .then(obj => JSON.stringify(obj)) + .catch(err => { + console.log('Invalid JSON', err) + return undefined + }) + : undefined, + } + }, + multiple: async io => { + const files = await io.input + .file('Upload an image!', { + helpText: + 'Will be uploaded to Interval and expire after the action finishes running.', + allowedExtensions: ['.gif', '.jpg', '.jpeg', '.png'], }) - : undefined, - json: rest.type.includes('text/') - ? await json() - .then(obj => JSON.stringify(obj)) - .catch(err => { - console.log('Invalid JSON', err) - return undefined - }) - : undefined, - } - }, + .multiple() + .optional() + + if (!files) return 'None selected.' + + await io.group( + ( + await Promise.all( + files.map(async file => [ + io.display.image(file.name, { + url: await file.url(), + }), + ]) + ) + ).map(([p]) => p) + ) + + return Object.fromEntries(files.map((file, i) => [i, file.name])) + }, + }, + }), advanced_data: async io => { const data = { bigInt: BigInt(5), diff --git a/src/ioSchema.ts b/src/ioSchema.ts index d293412..75aa96d 100644 --- a/src/ioSchema.ts +++ b/src/ioSchema.ts @@ -661,13 +661,33 @@ const INPUT_SCHEMA = { props: z.object({ helpText: z.string().optional(), allowedExtensions: z.array(z.string()).optional(), + disabled: z.optional(z.boolean().default(false)), + fileUrls: z + .array( + z.object({ + uploadUrl: z.string(), + downloadUrl: z.string(), + }) + ) + .nullish(), + + // Deprecated uploadUrl: z.string().nullish().optional(), downloadUrl: z.string().nullish().optional(), - disabled: z.optional(z.boolean().default(false)), }), state: z.object({ - name: z.string(), - type: z.string(), + files: z + .array( + z.object({ + name: z.string(), + type: z.string(), + }) + ) + .optional(), + + // Deprecated + name: z.string().optional(), + type: z.string().optional(), }), returns: z.object({ name: z.string(), @@ -676,6 +696,7 @@ const INPUT_SCHEMA = { size: z.number(), url: z.string(), }), + supportsMultiple: true, }, SEARCH: { props: z.object({