From 67d070532b9be7cca89897aeaeb61ee1d03d3cca Mon Sep 17 00:00:00 2001 From: Ryan Coppolo Date: Mon, 9 Jan 2023 15:00:31 -0500 Subject: [PATCH 1/4] io.input.file w/ multiple() --- src/classes/IOPromise.ts | 4 ++-- src/examples/basic/index.ts | 8 ++++---- src/ioSchema.ts | 1 + 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/classes/IOPromise.ts b/src/classes/IOPromise.ts index 3406694..26bd5f9 100644 --- a/src/classes/IOPromise.ts +++ b/src/classes/IOPromise.ts @@ -307,14 +307,14 @@ export class MultipleableIOPromise< multiple({ defaultValue }: { defaultValue?: DefaultValue[] } = {}) { 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/examples/basic/index.ts b/src/examples/basic/index.ts index 3ddddd1..36a17ab 100644 --- a/src/examples/basic/index.ts +++ b/src/examples/basic/index.ts @@ -1325,15 +1325,15 @@ const interval = new Interval({ console.log(await customDestinationFile.url()) - const file = await io.input.file('Upload an image!', { + 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'], - }) + }).multiple() - console.log(file) + console.log(files) - const { text, json, buffer, url, ...rest } = file + const { text, json, buffer, url, ...rest } = files[0] return { ...rest, diff --git a/src/ioSchema.ts b/src/ioSchema.ts index d293412..cc88dc3 100644 --- a/src/ioSchema.ts +++ b/src/ioSchema.ts @@ -676,6 +676,7 @@ const INPUT_SCHEMA = { size: z.number(), url: z.string(), }), + supportsMultiple: true, }, SEARCH: { props: z.object({ From 40ea50da1f7d11151d1a660c634f34a19c4ba507 Mon Sep 17 00:00:00 2001 From: Jacob Mischka Date: Wed, 11 Jan 2023 18:13:31 -0600 Subject: [PATCH 2/4] Display uploaded images in upload example action --- src/examples/basic/index.ts | 36 +++++++++++++++++++++++++----------- 1 file changed, 25 insertions(+), 11 deletions(-) diff --git a/src/examples/basic/index.ts b/src/examples/basic/index.ts index 36a17ab..1b33d23 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. @@ -1325,13 +1325,27 @@ const interval = new Interval({ console.log(await customDestinationFile.url()) - 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'], - }).multiple() - - console.log(files) + 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'], + }) + .multiple() + + if (files) { + await io.group( + ( + await Promise.all( + files.map(async file => [ + io.display.image(file.name, { + url: await file.url(), + }), + ]) + ) + ).map(([p]) => p) + ) + } const { text, json, buffer, url, ...rest } = files[0] From 51dcfc7e8c9ec380a0db5a7dc0ab9f8724617428 Mon Sep 17 00:00:00 2001 From: Jacob Mischka Date: Thu, 12 Jan 2023 12:04:55 -0600 Subject: [PATCH 3/4] Fix multi-file upload, support lists of upload/download url sets --- src/components/upload.ts | 23 +++---- src/examples/basic/index.ts | 116 ++++++++++++++++++++---------------- src/ioSchema.ts | 26 +++++++- 3 files changed, 95 insertions(+), 70 deletions(-) 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 1b33d23..62e44eb 100644 --- a/src/examples/basic/index.ts +++ b/src/examples/basic/index.ts @@ -1311,63 +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) - }, - }) + 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}` - console.log(await customDestinationFile.url()) + return generateS3Urls(path) + }, + } + ) - 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'], - }) - .multiple() - - if (files) { - await io.group( - ( - await Promise.all( - files.map(async file => [ - io.display.image(file.name, { - url: await file.url(), - }), - ]) - ) - ).map(([p]) => p) - ) - } + console.log(await customDestinationFile.url()) - const { text, json, buffer, url, ...rest } = files[0] + 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 cc88dc3..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(), From 092f6bb1089737ff2ac1b23195e8f90d0e375c93 Mon Sep 17 00:00:00 2001 From: Jacob Mischka Date: Thu, 12 Jan 2023 12:30:37 -0600 Subject: [PATCH 4/4] Set multiple DefaultValue type to never if unsupported for method --- src/classes/IOPromise.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/classes/IOPromise.ts b/src/classes/IOPromise.ts index 26bd5f9..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,7 +307,11 @@ export class MultipleableIOPromise< this.defaultValueGetter = defaultValueGetter } - multiple({ defaultValue }: { defaultValue?: DefaultValue[] } = {}) { + multiple({ + defaultValue, + }: { + defaultValue?: DefaultValue[] + } = {}): MultipleIOPromise { let transformedDefaultValue: T_IO_RETURNS[] | undefined const propsSchema = ioSchema[this.methodName].props if (defaultValue && 'defaultValue' in propsSchema.shape) {