From 6804d2897a7839ab9b06ef54307b6f0c5f350af7 Mon Sep 17 00:00:00 2001 From: Jacob Mischka Date: Wed, 29 Jun 2022 04:36:55 -0500 Subject: [PATCH 1/6] Allow conditionally marking an IOPromise as optional --- 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 9601e5c..b9dc1c4 100644 --- a/src/classes/IOPromise.ts +++ b/src/classes/IOPromise.ts @@ -77,8 +77,14 @@ export class IOPromise< ) } - optional(): OptionalIOPromise { - return new OptionalIOPromise(this) + optional(isOptional?: true): OptionalIOPromise + optional(isOptional?: false): IOPromise + optional( + isOptional = true + ): + | OptionalIOPromise + | IOPromise { + return isOptional ? new OptionalIOPromise(this) : this } } From bcf9ad9ce7693deecf00d8d858b92a9a1eba554e Mon Sep 17 00:00:00 2001 From: Jacob Mischka Date: Wed, 29 Jun 2022 07:25:24 -0500 Subject: [PATCH 2/6] Add type overrides to indicate whether io function props are required --- src/classes/IOClient.ts | 59 +++++++++++++++++++++++++++++++++-------- src/types.ts | 18 +++++++++++++ 2 files changed, 66 insertions(+), 11 deletions(-) diff --git a/src/classes/IOClient.ts b/src/classes/IOClient.ts index 8a07b2c..1f44cf5 100644 --- a/src/classes/IOClient.ts +++ b/src/classes/IOClient.ts @@ -30,6 +30,8 @@ import { ExclusiveIOComponentFunction, ComponentRenderer, IOComponentDefinition, + RequiredPropsIOComponentFunction, + RequiredPropsExclusiveIOComponentFunction, } from '../types' interface ClientConfig { @@ -237,8 +239,29 @@ export class IOClient { Output = T_IO_RETURNS >( methodName: MethodName, + propsRequired?: false, componentDef?: IOComponentDefinition - ): IOComponentFunction { + ): IOComponentFunction + createIOMethod< + MethodName extends T_IO_METHOD_NAMES, + Props extends object = T_IO_PROPS, + Output = T_IO_RETURNS + >( + methodName: MethodName, + propsRequired?: true, + componentDef?: IOComponentDefinition + ): RequiredPropsIOComponentFunction + createIOMethod< + MethodName extends T_IO_METHOD_NAMES, + Props extends object = T_IO_PROPS, + Output = T_IO_RETURNS + >( + methodName: MethodName, + _propsRequired = false, + componentDef?: IOComponentDefinition + ): + | IOComponentFunction + | RequiredPropsIOComponentFunction { return (label: string, props?: Props) => { let internalProps = props ? (props as T_IO_PROPS) : {} let getValue = (r: T_IO_RETURNS) => r as unknown as Output @@ -280,7 +303,16 @@ export class IOClient { * ExclusiveIOPromise, which cannot be rendered in a group. */ makeExclusive( - inner: IOComponentFunction + inner: IOComponentFunction, + propsRequired: false + ): ExclusiveIOComponentFunction + makeExclusive( + inner: IOComponentFunction, + propsRequired?: true + ): RequiredPropsExclusiveIOComponentFunction + makeExclusive( + inner: IOComponentFunction, + _propsRequired = false ): ExclusiveIOComponentFunction { return (label: string, props?: Props) => { return new ExclusiveIOPromise(inner(label, props)) @@ -296,7 +328,7 @@ export class IOClient { confirm: this.makeExclusive(this.createIOMethod('CONFIRM')), - search: this.createIOMethod('SEARCH', search), + search: this.createIOMethod('SEARCH', true, search), input: { text: this.createIOMethod('INPUT_TEXT'), @@ -306,28 +338,33 @@ export class IOClient { richText: this.createIOMethod('INPUT_RICH_TEXT'), }, select: { - single: this.createIOMethod('SELECT_SINGLE', selectSingle), - multiple: this.createIOMethod('SELECT_MULTIPLE', selectMultiple), - table: this.createIOMethod('SELECT_TABLE', selectTable), + single: this.createIOMethod('SELECT_SINGLE', true, selectSingle), + multiple: this.createIOMethod('SELECT_MULTIPLE', true, selectMultiple), + table: this.createIOMethod('SELECT_TABLE', true, selectTable), }, display: { heading: this.createIOMethod('DISPLAY_HEADING'), markdown: this.createIOMethod('DISPLAY_MARKDOWN'), link: this.createIOMethod('DISPLAY_LINK'), object: this.createIOMethod('DISPLAY_OBJECT'), - table: this.createIOMethod('DISPLAY_TABLE', displayTable), + table: this.createIOMethod('DISPLAY_TABLE', true, displayTable), }, experimental: { - spreadsheet: this.createIOMethod('INPUT_SPREADSHEET', spreadsheet), + spreadsheet: this.createIOMethod( + 'INPUT_SPREADSHEET', + true, + spreadsheet + ), findAndSelectUser: this.createIOMethod( 'SELECT_USER', + true, findAndSelectUser ), - date: this.createIOMethod('INPUT_DATE', date), + date: this.createIOMethod('INPUT_DATE', false, date), time: this.createIOMethod('INPUT_TIME'), - datetime: this.createIOMethod('INPUT_DATETIME', datetime), + datetime: this.createIOMethod('INPUT_DATETIME', false, datetime), input: { - file: this.createIOMethod('UPLOAD_FILE', file), + file: this.createIOMethod('UPLOAD_FILE', false, file), }, }, } diff --git a/src/types.ts b/src/types.ts index 9d39642..1bb1368 100644 --- a/src/types.ts +++ b/src/types.ts @@ -60,6 +60,24 @@ export type IntervalActionDefinition = | IntervalActionHandler | ExplicitIntervalActionDefinition +export type RequiredPropsIOComponentFunction< + MethodName extends T_IO_METHOD_NAMES, + Props, + Output = ComponentReturnValue +> = ( + label: string, + props: Props +) => IOPromise, Output> + +export type RequiredPropsExclusiveIOComponentFunction< + MethodName extends T_IO_METHOD_NAMES, + Props, + Output = ComponentReturnValue +> = ( + label: string, + props: Props +) => ExclusiveIOPromise, Output> + export type IOComponentFunction< MethodName extends T_IO_METHOD_NAMES, Props, From 0bf354766df9ee62233e66e16b9546ef995132db Mon Sep 17 00:00:00 2001 From: Jacob Mischka Date: Wed, 29 Jun 2022 07:28:39 -0500 Subject: [PATCH 3/6] Remove io.experimental.findAndSelectUser in favor of io.search It's not used by anyone in production and not published on npm. --- src/classes/IOClient.ts | 6 ------ src/components/selectUser.ts | 14 -------------- src/ioSchema.ts | 19 ------------------- 3 files changed, 39 deletions(-) delete mode 100644 src/components/selectUser.ts diff --git a/src/classes/IOClient.ts b/src/classes/IOClient.ts index 1f44cf5..ed56fb6 100644 --- a/src/classes/IOClient.ts +++ b/src/classes/IOClient.ts @@ -14,7 +14,6 @@ import { IOPromise, ExclusiveIOPromise } from './IOPromise' import IOError from './IOError' import spreadsheet from '../components/spreadsheet' import { selectTable, displayTable } from '../components/table' -import findAndSelectUser from '../components/selectUser' import selectSingle from '../components/selectSingle' import search from '../components/search' import selectMultiple from '../components/selectMultiple' @@ -355,11 +354,6 @@ export class IOClient { true, spreadsheet ), - findAndSelectUser: this.createIOMethod( - 'SELECT_USER', - true, - findAndSelectUser - ), date: this.createIOMethod('INPUT_DATE', false, date), time: this.createIOMethod('INPUT_TIME'), datetime: this.createIOMethod('INPUT_DATETIME', false, datetime), diff --git a/src/components/selectUser.ts b/src/components/selectUser.ts deleted file mode 100644 index 6ac041f..0000000 --- a/src/components/selectUser.ts +++ /dev/null @@ -1,14 +0,0 @@ -import type { T_IO_PROPS, T_IO_STATE } from '../ioSchema' - -export default function findAndSelectUser( - props: T_IO_PROPS<'SELECT_USER'> & { - onSearch: (query: string) => Promise['userList']> - } -) { - return { - async onStateChange(newState: T_IO_STATE<'SELECT_USER'>) { - const filteredUsers = await props.onSearch(newState.queryTerm) - return { userList: filteredUsers } - }, - } -} diff --git a/src/ioSchema.ts b/src/ioSchema.ts index 415215e..1a808cd 100644 --- a/src/ioSchema.ts +++ b/src/ioSchema.ts @@ -388,25 +388,6 @@ export const ioSchema = { state: z.null(), returns: z.array(labelValue), }, - SELECT_USER: { - props: z.object({ - userList: z.array( - z.object({ - id: z.union([z.string(), z.number()]), - name: z.string(), - email: z.string().optional(), - imageUrl: z.string().optional(), - }) - ), - }), - state: z.object({ queryTerm: z.string() }), - returns: z.object({ - id: z.union([z.string(), z.number()]), - name: z.string(), - email: z.string().optional(), - imageUrl: z.string().optional(), - }), - }, DISPLAY_HEADING: { props: z.object({}), state: z.null(), From ec10ef0ce0fa26cb3c4d4aceb8dfada387b71d2e Mon Sep 17 00:00:00 2001 From: Jacob Mischka Date: Wed, 29 Jun 2022 08:59:04 -0500 Subject: [PATCH 4/6] Strip defined-yet-undefined values from props before superjson Superjson distinguishes defined values with the value of `undefined`, so we strip them before passing them through. This doesn't really affect anything in practice, since when deserializing they'll be recast to `undefined` and then discarded, but this prevents them from being sent over the wire at all. This was really only a potential problem with a few of our custom component overrides (io.search, etc), because we define props statically but set them to undefined in order to convert the types, but could potentially happen with conditional values in userland code as well. Closes #642 --- src/classes/IOClient.ts | 3 ++- src/components/search.ts | 6 ++---- src/utils/deserialize.ts | 17 +++++++++++++++++ 3 files changed, 21 insertions(+), 5 deletions(-) diff --git a/src/classes/IOClient.ts b/src/classes/IOClient.ts index ed56fb6..737eef6 100644 --- a/src/classes/IOClient.ts +++ b/src/classes/IOClient.ts @@ -32,6 +32,7 @@ import { RequiredPropsIOComponentFunction, RequiredPropsExclusiveIOComponentFunction, } from '../types' +import { stripUndefined } from '../utils/deserialize' interface ClientConfig { logger: Logger @@ -91,7 +92,7 @@ export class IOClient { toRender: components .map(c => c.getRenderInfo()) .map(({ props, ...renderInfo }) => { - const { json, meta } = superjson.serialize(props) + const { json, meta } = superjson.serialize(stripUndefined(props)) return { ...renderInfo, props: json, diff --git a/src/components/search.ts b/src/components/search.ts index c0de7e6..3943dba 100644 --- a/src/components/search.ts +++ b/src/components/search.ts @@ -14,9 +14,8 @@ type InternalResults = T_IO_PROPS<'SEARCH'>['results'] export default function search({ onSearch, initialResults = [], - placeholder, renderResult, - helpText, + ...rest }: { placeholder?: string helpText?: string @@ -54,8 +53,7 @@ export default function search({ } const props: T_IO_PROPS<'SEARCH'> = { - placeholder, - helpText, + ...rest, results: renderResults(initialResults), } diff --git a/src/utils/deserialize.ts b/src/utils/deserialize.ts index 8e0bdf7..87e3139 100644 --- a/src/utils/deserialize.ts +++ b/src/utils/deserialize.ts @@ -55,3 +55,20 @@ export function deserializeDates( return ret } + +export function stripUndefined< + K extends string | number | symbol, + V, + T extends Record | undefined +>(obj: T): T { + if (!obj) return obj + + const newObj = { ...obj } as Exclude + for (const [key, val] of Object.entries(newObj)) { + if (val === undefined) { + delete newObj[key as K] + } + } + + return newObj +} From d8e4b12ed70c8540efd8684f2d7f69eea353bd07 Mon Sep 17 00:00:00 2001 From: Jacob Mischka Date: Wed, 29 Jun 2022 12:22:50 -0500 Subject: [PATCH 5/6] Allow specifying action name and description in action definitions --- src/examples/basic/index.ts | 2 ++ src/internalRpcSchema.ts | 2 ++ src/types.ts | 2 ++ 3 files changed, 6 insertions(+) diff --git a/src/examples/basic/index.ts b/src/examples/basic/index.ts index ae6a333..7359e43 100644 --- a/src/examples/basic/index.ts +++ b/src/examples/basic/index.ts @@ -12,6 +12,8 @@ const prod = new Interval({ actions: { ImportUsers: { backgroundable: true, + name: 'Import users', + description: "Doesn't actually import users", handler: async io => { console.log("I'm a live mode action") const name = await io.input.text('Enter the name for a user') diff --git a/src/internalRpcSchema.ts b/src/internalRpcSchema.ts index 74c3f6c..183bcc5 100644 --- a/src/internalRpcSchema.ts +++ b/src/internalRpcSchema.ts @@ -42,6 +42,8 @@ export type LoadingState = z.input export const ACTION_DEFINITION = z.object({ slug: z.string(), + name: z.string().optional(), + description: z.string().optional(), backgroundable: z.boolean().optional(), }) diff --git a/src/types.ts b/src/types.ts index 1bb1368..3155ea1 100644 --- a/src/types.ts +++ b/src/types.ts @@ -54,6 +54,8 @@ export interface IntervalActionStore { export interface ExplicitIntervalActionDefinition { handler: IntervalActionHandler backgroundable?: boolean + name?: string + description?: string } export type IntervalActionDefinition = From 8b7ca8050e5424bec5ca39ea82a2c12a274fe006 Mon Sep 17 00:00:00 2001 From: Jacob Mischka Date: Wed, 29 Jun 2022 13:03:06 -0500 Subject: [PATCH 6/6] Add third type override for the dynamic boolean case Forgot that the actual definition's types are just for us and are ignored externally. --- src/classes/IOPromise.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/classes/IOPromise.ts b/src/classes/IOPromise.ts index b9dc1c4..4fe77ed 100644 --- a/src/classes/IOPromise.ts +++ b/src/classes/IOPromise.ts @@ -79,6 +79,11 @@ export class IOPromise< optional(isOptional?: true): OptionalIOPromise optional(isOptional?: false): IOPromise + optional( + isOptional?: boolean + ): + | OptionalIOPromise + | IOPromise optional( isOptional = true ):