import { AnyStateMachine, ContextFrom, EventFrom, StateFrom } from 'xstate' import { isTauri } from './isTauri' import { CustomIconName } from 'components/CustomIcon' type Icon = CustomIconName type Platform = 'both' | 'web' | 'desktop' type InputType = 'select' | 'string' | 'interaction' export type CommandArgumentOption = { name: string; isCurrent?: boolean } // Command arguments can either be defined manually // or flagged as needing to be looked up from the context. // This is useful for things like settings, where // we want to show the current setting value as the default. // The lookup is done in createMachineCommand. type CommandArgumentConfig = { name: string // TODO: I would love for this to be strongly-typed so we could guarantee it's a valid data payload key on the event type. type: InputType description?: string } & ( | { type: 'select' options?: CommandArgumentOption[] getOptionsFromContext?: keyof ContextFrom defaultValue?: string getDefaultValueFromContext?: keyof ContextFrom } | { type: 'string' defaultValue?: string getDefaultValueFromContext?: keyof ContextFrom } | { type: 'interaction' } ) export type CommandBarConfig = Partial<{ [EventType in EventFrom['type']]: | { args: CommandArgumentConfig[] formatFunction?: (args: string[]) => string icon?: Icon hide?: Platform } | { hide?: Platform } }> export type Command = { owner: string name: string callback: Function icon?: Icon args?: CommandArgument[] formatFunction?: (args: string[]) => string } export type CommandArgument = { name: string defaultValue?: string } & ( | { type: Extract options: CommandArgumentOption[] } | { type: Exclude } ) interface CreateMachineCommandProps { type: EventFrom['type'] state: StateFrom commandBarConfig?: CommandBarConfig send: Function owner: string } // Creates a command with subcommands, ready for use in the CommandBar component, // from a more terse Command Bar Meta definition. export function createMachineCommand({ type, state, commandBarConfig, send, owner, }: CreateMachineCommandProps): Command | null { const lookedUpMeta = commandBarConfig && commandBarConfig[type] if (!lookedUpMeta) return null // Hide commands based on platform by returning `null` // so the consumer can filter them out if ('hide' in lookedUpMeta) { const { hide } = lookedUpMeta if (hide === 'both') return null else if (hide === 'desktop' && isTauri()) return null else if (hide === 'web' && !isTauri()) return null } const icon = ('icon' in lookedUpMeta && lookedUpMeta.icon) || undefined const formatFunction = ('formatFunction' in lookedUpMeta && lookedUpMeta.formatFunction) || undefined return { name: type, owner, icon, callback: (data: EventFrom) => { if (data !== undefined && data !== null) { send(type, { data }) } else { send(type) } }, ...('args' in lookedUpMeta ? { args: getCommandArgumentValuesFromContext(state, lookedUpMeta.args), formatFunction, } : {}), } } function getCommandArgumentValuesFromContext( state: StateFrom, args: CommandArgumentConfig[] ): CommandArgument[] { function getDefaultValue( arg: CommandArgumentConfig & { type: 'string' | 'select' } ) { if ( arg.type === 'select' || ('getDefaultValueFromContext' in arg && arg.getDefaultValueFromContext) ) { return state.context[arg.getDefaultValueFromContext] } else { return arg.defaultValue } } return args.map((arg) => { switch (arg.type) { case 'interaction': return { name: arg.name, type: 'interaction', } case 'string': return { name: arg.name, type: arg.type, defaultValue: arg.getDefaultValueFromContext ? state.context[arg.getDefaultValueFromContext] : arg.defaultValue, } default: return { name: arg.name, type: arg.type, defaultValue: getDefaultValue(arg), options: arg.getOptionsFromContext ? state.context[arg.getOptionsFromContext].map( (v: string | { name: string }) => ({ name: typeof v === 'string' ? v : v.name, isCurrent: v === getDefaultValue(arg), }) ) : arg.getDefaultValueFromContext ? arg.options?.map((v) => ({ ...v, isCurrent: v.name === getDefaultValue(arg), })) : arg.options, } } }) }