Command bar: add extrude command, nonlinear editing, etc (#1204)

* Tweak toaster look and feel

* Add icons, tweak plus icon names

* Rename commandBarMeta to commandBarConfig

* Refactor command bar, add support for icons

* Create a tailwind plugin for aria-pressed button state

* Remove overlay from behind command bar

* Clean up toolbar

* Button and other style tweaks

* Icon tweaks follow-up: make old icons work with new sizing

* Delete unused static icons

* More CSS tweaks

* Small CSS tweak to project sidebar

* Add command bar E2E test

* fumpt

* A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu)

* fix typo in a comment

* Fix icon padding (built version only)

* Update onboarding and warning banner icons padding

* Misc minor style fixes

* Get Extrude opening and canceling from command bar

* Iconography tweaks

* Get extrude kind of working

* Refactor command bar config types and organization

* Move command bar configs to be co-located with each other

* Start building a state machine for the command bar

* Start converting command bar to state machine

* Add support for multiple args, confirmation step

* Submission behavior, hotkeys, code organization

* Add new test for extruding from command bar

* Polish step back and selection hotkeys, CSS tweaks

* Loading style tweaks

* Validate selection inputs, polish UX of args re-editing

* Prevent submission with multiple selection on singlular arg

* Remove stray console logs

* Tweak test, CSS nit, remove extrude "result" argument

* Fix linting warnings

* Show Ctrl+/ instead of ⌘K on all platforms but Mac

* A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu)

* Add "Enter sketch" to command bar

* fix command bar test

* Fix flaky cmd bar extrude test by waiting for engine select response

* Cover both button labels '⌘K' and 'Ctrl+/' in test

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
This commit is contained in:
Frank Noirot
2023-12-13 12:49:01 -05:00
committed by GitHub
parent b01357b49e
commit d2535bb8c2
60 changed files with 4549 additions and 1939 deletions

View File

@ -0,0 +1,158 @@
import { AnyStateMachine, EventFrom, InterpreterFrom, StateFrom } from 'xstate'
import { isTauri } from './isTauri'
import {
Command,
CommandArgument,
CommandArgumentConfig,
CommandConfig,
CommandSetConfig,
CommandSetSchema,
} from './commandTypes'
interface CreateMachineCommandProps<
T extends AnyStateMachine,
S extends CommandSetSchema<T>
> {
type: EventFrom<T>['type']
ownerMachine: T['id']
state: StateFrom<T>
send: Function
actor?: InterpreterFrom<T>
commandBarConfig?: CommandSetConfig<T, S>
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 CommandSetSchema<T>
>({
ownerMachine,
type,
state,
send,
actor,
commandBarConfig,
onCancel,
}: CreateMachineCommandProps<T, S>): Command<
T,
typeof type,
S[typeof type]
> | null {
const commandConfig = commandBarConfig && commandBarConfig[type]
if (!commandConfig) return null
// 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' && isTauri()) return null
else if (hide === 'web' && !isTauri()) return null
}
const icon = ('icon' in commandConfig && commandConfig.icon) || undefined
const command: Command<T, typeof type, S[typeof type]> = {
name: type,
ownerMachine: ownerMachine,
icon,
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
}
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 CommandSetSchema<T>,
CommandName extends EventFrom<T>['type'] = EventFrom<T>['type']
>(
state: StateFrom<T>,
args: CommandConfig<T, CommandName, S>['args'],
actor?: InterpreterFrom<T>
): NonNullable<Command<T, CommandName, S>['args']> {
const newArgs = {} as NonNullable<Command<T, CommandName, S>['args']>
for (const arg in args) {
const argConfig = args[arg] as CommandArgumentConfig<S[typeof arg], T>
const newArg = buildCommandArgument(argConfig, state, actor)
newArgs[arg] = newArg
}
return newArgs
}
function buildCommandArgument<
O extends CommandSetSchema<T>,
T extends AnyStateMachine
>(
arg: CommandArgumentConfig<O, T>,
state: StateFrom<T>,
actor?: InterpreterFrom<T>
): CommandArgument<O, T> & { inputType: typeof arg.inputType } {
const baseCommandArgument = {
description: arg.description,
required: arg.required,
payload: arg.payload,
defaultValue:
arg.defaultValue instanceof Function
? arg.defaultValue(state.context)
: arg.defaultValue,
} satisfies Omit<CommandArgument<O, T>, 'inputType'>
if (arg.inputType === 'options') {
const options = arg.options
? arg.options instanceof Function
? arg.options(state.context)
: arg.options
: undefined
if (!options) {
throw new Error('Options must be provided for options input type')
}
return {
inputType: arg.inputType,
...baseCommandArgument,
options,
} satisfies CommandArgument<O, T> & { inputType: 'options' }
} else if (arg.inputType === 'selection') {
if (!actor)
throw new Error('Actor must be provided for selection input type')
return {
inputType: arg.inputType,
...baseCommandArgument,
multiple: arg.multiple,
selectionTypes: arg.selectionTypes,
actor,
} satisfies CommandArgument<O, T> & { inputType: 'selection' }
} else {
return {
inputType: arg.inputType,
...baseCommandArgument,
}
}
}