Register modeling commands as disabled, enable without needing to re-open the command palette (#6886)

* Allow adding and removing commands from any command bar state

* Allow commands to be configured disabled in the combobox

* Set up modeling commands to toggle `disabled` based on network status, instead of filtering

* Fix tsc
This commit is contained in:
Frank Noirot
2025-05-12 21:18:50 -04:00
committed by GitHub
parent e7ecd655c4
commit 47feae3bd9
6 changed files with 68 additions and 53 deletions

View File

@ -1,6 +1,6 @@
import { Combobox } from '@headlessui/react'
import Fuse from 'fuse.js'
import { useEffect, useState } from 'react'
import { useEffect, useMemo, useState } from 'react'
import { CustomIcon } from '@src/components/CustomIcon'
import type { Command } from '@src/lib/commandTypes'
@ -21,13 +21,17 @@ function CommandComboBox({
const defaultOption =
options.find((o) => 'isCurrent' in o && o.isCurrent) || null
// sort disabled commands to the bottom
const sortedOptions = options
const sortedOptions = useMemo(
() =>
options
.map((command) => ({
command,
disabled: optionIsDisabled(command),
}))
.sort(sortCommands)
.map(({ command }) => command)
.map(({ command }) => command),
[options]
)
const fuse = new Fuse(sortedOptions, {
keys: ['displayName', 'name', 'description'],
@ -38,7 +42,7 @@ function CommandComboBox({
useEffect(() => {
const results = fuse.search(query).map((result) => result.item)
setFilteredOptions(query.length > 0 ? results : sortedOptions)
}, [query])
}, [query, sortedOptions])
function handleSelection(command: Command) {
commandBarActor.send({ type: 'Select command', data: { command } })
@ -122,10 +126,18 @@ function CommandComboBox({
export default CommandComboBox
/**
* If the command is configured to be disabled,
* or it is powered by a state machine and the event it is
* associated with is unavailable, disabled it.
*/
function optionIsDisabled(option: Command): boolean {
return (
'machineActor' in option &&
option.disabled ||
('machineActor' in option &&
option.machineActor !== undefined &&
!getActorNextEvents(option.machineActor.getSnapshot()).includes(option.name)
!getActorNextEvents(option.machineActor.getSnapshot()).includes(
option.name
))
)
}

View File

@ -34,7 +34,7 @@ import {
useMenuListener,
useSketchModeMenuEnableDisable,
} from '@src/hooks/useMenu'
import useStateMachineCommands from '@src/hooks/useStateMachineCommands'
import useModelingMachineCommands from '@src/hooks/useStateMachineCommands'
import { useKclContext } from '@src/lang/KclProvider'
import { updateModelingState } from '@src/lang/modelingWorkflows'
import {
@ -2091,13 +2091,12 @@ export const ModelingMachineProvider = ({
modelingSend({ type: 'Center camera on selection' })
})
useStateMachineCommands({
useModelingMachineCommands({
machineId: 'modeling',
state: modelingState,
send: modelingSend,
actor: modelingActor,
commandBarConfig: modelingMachineCommandConfig,
allCommandsRequireNetwork: true,
// TODO for when sketch tools are in the toolbar: This was added when we used one "Cancel" event,
// but we need to support "SketchCancel" and basically
// make this function take the actor or state so it

View File

@ -31,10 +31,16 @@ interface UseStateMachineCommandsArgs<
send: Function
actor: Actor<T>
commandBarConfig?: StateMachineCommandSetConfig<T, S>
allCommandsRequireNetwork?: boolean
onCancel?: () => void
}
/**
* @deprecated the type plumbing required for this function is way over-complicated.
* Instead, opt to create `Commands` directly.
*
* This is only used for modelingMachine commands now, and once that is decoupled from React,
* TODO: Delete this function and other state machine helper functions.
*/
export default function useStateMachineCommands<
T extends AnyStateMachine,
S extends StateMachineCommandSetSchema<T>,
@ -44,21 +50,19 @@ export default function useStateMachineCommands<
send,
actor,
commandBarConfig,
allCommandsRequireNetwork = false,
onCancel,
}: UseStateMachineCommandsArgs<T, S>) {
const { overallState } = useNetworkContext()
const { isExecuting } = useKclContext()
const { isStreamReady } = useAppState()
useEffect(() => {
const disableAllButtons =
const shouldDisableEngineCommands =
(overallState !== NetworkHealthState.Ok &&
overallState !== NetworkHealthState.Weak) ||
isExecuting ||
!isStreamReady
useEffect(() => {
const newCommands = Object.keys(commandBarConfig || {})
.filter((_) => !allCommandsRequireNetwork || !disableAllButtons)
.flatMap((type) => {
const typeWithProperType = type as EventFrom<T>['type']
return createMachineCommand<T, S>({
@ -70,6 +74,7 @@ export default function useStateMachineCommands<
actor,
commandBarConfig,
onCancel,
forceDisable: shouldDisableEngineCommands,
})
})
.filter((c) => c !== null) as Command[] // TS isn't smart enough to know this filter removes nulls
@ -85,5 +90,5 @@ export default function useStateMachineCommands<
data: { commands: newCommands },
})
}
}, [overallState, isExecuting, isStreamReady])
}, [shouldDisableEngineCommands, commandBarConfig])
}

View File

@ -103,6 +103,7 @@ export type Command<
icon?: Icon
hide?: PLATFORM[number]
hideFromSearch?: boolean
disabled?: boolean
status?: CommandStatus
}

View File

@ -29,6 +29,7 @@ interface CreateMachineCommandProps<
actor: Actor<T>
commandBarConfig?: StateMachineCommandSetConfig<T, S>
onCancel?: () => void
forceDisable?: boolean
}
// Creates a command with subcommands, ready for use in the CommandBar component,
@ -44,6 +45,7 @@ export function createMachineCommand<
actor,
commandBarConfig,
onCancel,
forceDisable = false,
}: CreateMachineCommandProps<T, S>):
| Command<T, typeof type, S[typeof type]>
| Command<T, typeof type, S[typeof type]>[]
@ -71,6 +73,7 @@ export function createMachineCommand<
actor,
commandBarConfig: recursiveCommandBarConfig,
onCancel,
forceDisable,
})
})
.filter((c) => c !== null) as Command<T, typeof type, S[typeof type]>[]
@ -105,6 +108,7 @@ export function createMachineCommand<
send({ type })
}
},
disabled: forceDisable,
}
if (commandConfig.args) {

View File

@ -459,35 +459,6 @@ export const commandBarMachine = setup({
Open: {
target: 'Selecting command',
},
'Add commands': {
target: 'Closed',
actions: [
assign({
commands: ({ context, event }) =>
[...context.commands, ...event.data.commands].sort(
sortCommands
),
}),
],
},
'Remove commands': {
target: 'Closed',
actions: [
assign({
commands: ({ context, event }) =>
context.commands.filter(
(c) =>
!event.data.commands.some(
(c2) => c2.name === c.name && c2.groupId === c.groupId
)
),
}),
],
},
},
always: {
@ -645,6 +616,29 @@ export const commandBarMachine = setup({
target: '.Command selected',
actions: ['Find and select command', 'Initialize arguments to submit'],
},
'Add commands': {
actions: [
assign({
commands: ({ context, event }) =>
[...context.commands, ...event.data.commands].sort(sortCommands),
}),
],
},
'Remove commands': {
actions: [
assign({
commands: ({ context, event }) =>
context.commands.filter(
(c) =>
!event.data.commands.some(
(c2) => c2.name === c.name && c2.groupId === c.groupId
)
),
}),
],
},
},
})