import { AnyStateMachine, ContextFrom, EventFrom, Actor, StateFrom, } from 'xstate' import { isDesktop } from './isDesktop' import { Command, CommandArgument, CommandArgumentConfig, CommandConfig, StateMachineCommandSetConfig, StateMachineCommandSetSchema, } from './commandTypes' interface CreateMachineCommandProps< T extends AnyStateMachine, S extends StateMachineCommandSetSchema > { type: EventFrom['type'] groupId: T['id'] state: StateFrom send: Function actor: Actor commandBarConfig?: StateMachineCommandSetConfig onCancel?: () => void } // Creates a command with subcommands, ready for use in the CommandBar component, // from a more terse Command Bar Meta definition. export function createMachineCommand< T extends AnyStateMachine, S extends StateMachineCommandSetSchema >({ groupId, type, state, send, actor, commandBarConfig, onCancel, }: CreateMachineCommandProps): | Command | Command[] | null { const commandConfig = commandBarConfig && commandBarConfig[type] // There may be no command config for this event type, // or there may be multiple commands to create. if (!commandConfig) { return null } else if (commandConfig instanceof Array) { return commandConfig .map((config) => { const recursiveCommandBarConfig: Partial< StateMachineCommandSetConfig > = { [type]: config, } return createMachineCommand({ groupId, type, state, send, actor, commandBarConfig: recursiveCommandBarConfig, onCancel, }) }) .filter((c) => c !== null) as Command[] } // Hide commands based on platform by returning `null` // so the consumer can filter them out if ('hide' in commandConfig) { const { hide } = commandConfig if (hide === 'both') return null else if (hide === 'desktop' && isDesktop()) return null else if (hide === 'web' && !isDesktop()) return null } const icon = ('icon' in commandConfig && commandConfig.icon) || undefined const command: Command = { name: type, groupId, icon, description: commandConfig.description, needsReview: commandConfig.needsReview || false, onSubmit: (data?: S[typeof type]) => { if (data !== undefined && data !== null) { send({ type, data }) } else { send({ type }) } }, } if (commandConfig.args) { const newArgs = buildCommandArguments(state, commandConfig.args, actor) command.args = newArgs } if (onCancel) { command.onCancel = onCancel } if ('displayName' in commandConfig) { command.displayName = commandConfig.displayName } return command } // Takes the args from a CommandConfig and creates // a finalized CommandArgument object for each one, // bundled together into the args for a Command. function buildCommandArguments< T extends AnyStateMachine, S extends StateMachineCommandSetSchema, CommandName extends EventFrom['type'] = EventFrom['type'] >( state: StateFrom, args: CommandConfig['args'], machineActor: Actor ): NonNullable['args']> { const newArgs = {} as NonNullable['args']> for (const arg in args) { const argConfig = args[arg] as CommandArgumentConfig const newArg = buildCommandArgument(argConfig, state.context, machineActor) newArgs[arg] = newArg } return newArgs } export function buildCommandArgument< T extends AnyStateMachine, O extends StateMachineCommandSetSchema = StateMachineCommandSetSchema >( arg: CommandArgumentConfig, context: ContextFrom, machineActor: Actor ): CommandArgument & { inputType: typeof arg.inputType } { const baseCommandArgument = { description: arg.description, required: arg.required, skip: arg.skip, machineActor, valueSummary: arg.valueSummary, } satisfies Omit, 'inputType'> if (arg.inputType === 'options') { return { inputType: arg.inputType, ...baseCommandArgument, defaultValue: arg.defaultValueFromContext ? arg.defaultValueFromContext(context) : arg.defaultValue, options: arg.optionsFromContext ? arg.optionsFromContext(context) : arg.options, } satisfies CommandArgument & { inputType: 'options' } } else if (arg.inputType === 'selection') { return { inputType: arg.inputType, ...baseCommandArgument, multiple: arg.multiple, selectionTypes: arg.selectionTypes, } satisfies CommandArgument & { inputType: 'selection' } } else if (arg.inputType === 'kcl') { return { inputType: arg.inputType, defaultValue: arg.defaultValue, ...baseCommandArgument, } satisfies CommandArgument & { inputType: 'kcl' } } else { return { inputType: arg.inputType, defaultValue: arg.defaultValueFromContext ? arg.defaultValueFromContext(context) : arg.defaultValue, ...baseCommandArgument, } } }