diff --git a/src/classes/IOClient.ts b/src/classes/IOClient.ts index f706b3f..3c95d76 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' @@ -30,7 +29,10 @@ import { ExclusiveIOComponentFunction, ComponentRenderer, IOComponentDefinition, + RequiredPropsIOComponentFunction, + RequiredPropsExclusiveIOComponentFunction, } from '../types' +import { stripUndefined } from '../utils/deserialize' interface ClientConfig { logger: Logger @@ -90,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, @@ -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,36 @@ 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(this.logger)), + single: this.createIOMethod('SELECT_SINGLE', true, selectSingle), + multiple: this.createIOMethod('SELECT_MULTIPLE', true, selectMultiple), + table: this.createIOMethod( + 'SELECT_TABLE', + true, + selectTable(this.logger) + ), }, 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(this.logger)), + table: this.createIOMethod( + 'DISPLAY_TABLE', + true, + displayTable(this.logger) + ), }, experimental: { - spreadsheet: this.createIOMethod('INPUT_SPREADSHEET', spreadsheet), - findAndSelectUser: this.createIOMethod( - 'SELECT_USER', - findAndSelectUser + spreadsheet: this.createIOMethod( + 'INPUT_SPREADSHEET', + true, + spreadsheet ), - 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/classes/IOPromise.ts b/src/classes/IOPromise.ts index 9601e5c..4fe77ed 100644 --- a/src/classes/IOPromise.ts +++ b/src/classes/IOPromise.ts @@ -77,8 +77,19 @@ export class IOPromise< ) } - optional(): OptionalIOPromise { - return new OptionalIOPromise(this) + optional(isOptional?: true): OptionalIOPromise + optional(isOptional?: false): IOPromise + optional( + isOptional?: boolean + ): + | OptionalIOPromise + | IOPromise + optional( + isOptional = true + ): + | OptionalIOPromise + | IOPromise { + return isOptional ? new OptionalIOPromise(this) : this } } 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/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/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/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(), diff --git a/src/types.ts b/src/types.ts index 9d39642..3155ea1 100644 --- a/src/types.ts +++ b/src/types.ts @@ -54,12 +54,32 @@ export interface IntervalActionStore { export interface ExplicitIntervalActionDefinition { handler: IntervalActionHandler backgroundable?: boolean + name?: string + description?: string } 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, 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 +}