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:
@ -1,6 +1,6 @@
|
|||||||
import { Combobox } from '@headlessui/react'
|
import { Combobox } from '@headlessui/react'
|
||||||
import Fuse from 'fuse.js'
|
import Fuse from 'fuse.js'
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useMemo, useState } from 'react'
|
||||||
|
|
||||||
import { CustomIcon } from '@src/components/CustomIcon'
|
import { CustomIcon } from '@src/components/CustomIcon'
|
||||||
import type { Command } from '@src/lib/commandTypes'
|
import type { Command } from '@src/lib/commandTypes'
|
||||||
@ -21,13 +21,17 @@ function CommandComboBox({
|
|||||||
const defaultOption =
|
const defaultOption =
|
||||||
options.find((o) => 'isCurrent' in o && o.isCurrent) || null
|
options.find((o) => 'isCurrent' in o && o.isCurrent) || null
|
||||||
// sort disabled commands to the bottom
|
// sort disabled commands to the bottom
|
||||||
const sortedOptions = options
|
const sortedOptions = useMemo(
|
||||||
.map((command) => ({
|
() =>
|
||||||
command,
|
options
|
||||||
disabled: optionIsDisabled(command),
|
.map((command) => ({
|
||||||
}))
|
command,
|
||||||
.sort(sortCommands)
|
disabled: optionIsDisabled(command),
|
||||||
.map(({ command }) => command)
|
}))
|
||||||
|
.sort(sortCommands)
|
||||||
|
.map(({ command }) => command),
|
||||||
|
[options]
|
||||||
|
)
|
||||||
|
|
||||||
const fuse = new Fuse(sortedOptions, {
|
const fuse = new Fuse(sortedOptions, {
|
||||||
keys: ['displayName', 'name', 'description'],
|
keys: ['displayName', 'name', 'description'],
|
||||||
@ -38,7 +42,7 @@ function CommandComboBox({
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const results = fuse.search(query).map((result) => result.item)
|
const results = fuse.search(query).map((result) => result.item)
|
||||||
setFilteredOptions(query.length > 0 ? results : sortedOptions)
|
setFilteredOptions(query.length > 0 ? results : sortedOptions)
|
||||||
}, [query])
|
}, [query, sortedOptions])
|
||||||
|
|
||||||
function handleSelection(command: Command) {
|
function handleSelection(command: Command) {
|
||||||
commandBarActor.send({ type: 'Select command', data: { command } })
|
commandBarActor.send({ type: 'Select command', data: { command } })
|
||||||
@ -122,10 +126,18 @@ function CommandComboBox({
|
|||||||
|
|
||||||
export default 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 {
|
function optionIsDisabled(option: Command): boolean {
|
||||||
return (
|
return (
|
||||||
'machineActor' in option &&
|
option.disabled ||
|
||||||
option.machineActor !== undefined &&
|
('machineActor' in option &&
|
||||||
!getActorNextEvents(option.machineActor.getSnapshot()).includes(option.name)
|
option.machineActor !== undefined &&
|
||||||
|
!getActorNextEvents(option.machineActor.getSnapshot()).includes(
|
||||||
|
option.name
|
||||||
|
))
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -34,7 +34,7 @@ import {
|
|||||||
useMenuListener,
|
useMenuListener,
|
||||||
useSketchModeMenuEnableDisable,
|
useSketchModeMenuEnableDisable,
|
||||||
} from '@src/hooks/useMenu'
|
} from '@src/hooks/useMenu'
|
||||||
import useStateMachineCommands from '@src/hooks/useStateMachineCommands'
|
import useModelingMachineCommands from '@src/hooks/useStateMachineCommands'
|
||||||
import { useKclContext } from '@src/lang/KclProvider'
|
import { useKclContext } from '@src/lang/KclProvider'
|
||||||
import { updateModelingState } from '@src/lang/modelingWorkflows'
|
import { updateModelingState } from '@src/lang/modelingWorkflows'
|
||||||
import {
|
import {
|
||||||
@ -2091,13 +2091,12 @@ export const ModelingMachineProvider = ({
|
|||||||
modelingSend({ type: 'Center camera on selection' })
|
modelingSend({ type: 'Center camera on selection' })
|
||||||
})
|
})
|
||||||
|
|
||||||
useStateMachineCommands({
|
useModelingMachineCommands({
|
||||||
machineId: 'modeling',
|
machineId: 'modeling',
|
||||||
state: modelingState,
|
state: modelingState,
|
||||||
send: modelingSend,
|
send: modelingSend,
|
||||||
actor: modelingActor,
|
actor: modelingActor,
|
||||||
commandBarConfig: modelingMachineCommandConfig,
|
commandBarConfig: modelingMachineCommandConfig,
|
||||||
allCommandsRequireNetwork: true,
|
|
||||||
// TODO for when sketch tools are in the toolbar: This was added when we used one "Cancel" event,
|
// 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
|
// but we need to support "SketchCancel" and basically
|
||||||
// make this function take the actor or state so it
|
// make this function take the actor or state so it
|
||||||
|
@ -31,10 +31,16 @@ interface UseStateMachineCommandsArgs<
|
|||||||
send: Function
|
send: Function
|
||||||
actor: Actor<T>
|
actor: Actor<T>
|
||||||
commandBarConfig?: StateMachineCommandSetConfig<T, S>
|
commandBarConfig?: StateMachineCommandSetConfig<T, S>
|
||||||
allCommandsRequireNetwork?: boolean
|
|
||||||
onCancel?: () => void
|
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<
|
export default function useStateMachineCommands<
|
||||||
T extends AnyStateMachine,
|
T extends AnyStateMachine,
|
||||||
S extends StateMachineCommandSetSchema<T>,
|
S extends StateMachineCommandSetSchema<T>,
|
||||||
@ -44,21 +50,19 @@ export default function useStateMachineCommands<
|
|||||||
send,
|
send,
|
||||||
actor,
|
actor,
|
||||||
commandBarConfig,
|
commandBarConfig,
|
||||||
allCommandsRequireNetwork = false,
|
|
||||||
onCancel,
|
onCancel,
|
||||||
}: UseStateMachineCommandsArgs<T, S>) {
|
}: UseStateMachineCommandsArgs<T, S>) {
|
||||||
const { overallState } = useNetworkContext()
|
const { overallState } = useNetworkContext()
|
||||||
const { isExecuting } = useKclContext()
|
const { isExecuting } = useKclContext()
|
||||||
const { isStreamReady } = useAppState()
|
const { isStreamReady } = useAppState()
|
||||||
|
const shouldDisableEngineCommands =
|
||||||
|
(overallState !== NetworkHealthState.Ok &&
|
||||||
|
overallState !== NetworkHealthState.Weak) ||
|
||||||
|
isExecuting ||
|
||||||
|
!isStreamReady
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const disableAllButtons =
|
|
||||||
(overallState !== NetworkHealthState.Ok &&
|
|
||||||
overallState !== NetworkHealthState.Weak) ||
|
|
||||||
isExecuting ||
|
|
||||||
!isStreamReady
|
|
||||||
const newCommands = Object.keys(commandBarConfig || {})
|
const newCommands = Object.keys(commandBarConfig || {})
|
||||||
.filter((_) => !allCommandsRequireNetwork || !disableAllButtons)
|
|
||||||
.flatMap((type) => {
|
.flatMap((type) => {
|
||||||
const typeWithProperType = type as EventFrom<T>['type']
|
const typeWithProperType = type as EventFrom<T>['type']
|
||||||
return createMachineCommand<T, S>({
|
return createMachineCommand<T, S>({
|
||||||
@ -70,6 +74,7 @@ export default function useStateMachineCommands<
|
|||||||
actor,
|
actor,
|
||||||
commandBarConfig,
|
commandBarConfig,
|
||||||
onCancel,
|
onCancel,
|
||||||
|
forceDisable: shouldDisableEngineCommands,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.filter((c) => c !== null) as Command[] // TS isn't smart enough to know this filter removes nulls
|
.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 },
|
data: { commands: newCommands },
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}, [overallState, isExecuting, isStreamReady])
|
}, [shouldDisableEngineCommands, commandBarConfig])
|
||||||
}
|
}
|
||||||
|
@ -103,6 +103,7 @@ export type Command<
|
|||||||
icon?: Icon
|
icon?: Icon
|
||||||
hide?: PLATFORM[number]
|
hide?: PLATFORM[number]
|
||||||
hideFromSearch?: boolean
|
hideFromSearch?: boolean
|
||||||
|
disabled?: boolean
|
||||||
status?: CommandStatus
|
status?: CommandStatus
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -29,6 +29,7 @@ interface CreateMachineCommandProps<
|
|||||||
actor: Actor<T>
|
actor: Actor<T>
|
||||||
commandBarConfig?: StateMachineCommandSetConfig<T, S>
|
commandBarConfig?: StateMachineCommandSetConfig<T, S>
|
||||||
onCancel?: () => void
|
onCancel?: () => void
|
||||||
|
forceDisable?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
// Creates a command with subcommands, ready for use in the CommandBar component,
|
// Creates a command with subcommands, ready for use in the CommandBar component,
|
||||||
@ -44,6 +45,7 @@ export function createMachineCommand<
|
|||||||
actor,
|
actor,
|
||||||
commandBarConfig,
|
commandBarConfig,
|
||||||
onCancel,
|
onCancel,
|
||||||
|
forceDisable = false,
|
||||||
}: CreateMachineCommandProps<T, S>):
|
}: CreateMachineCommandProps<T, S>):
|
||||||
| Command<T, typeof type, S[typeof type]>
|
| Command<T, typeof type, S[typeof type]>
|
||||||
| Command<T, typeof type, S[typeof type]>[]
|
| Command<T, typeof type, S[typeof type]>[]
|
||||||
@ -71,6 +73,7 @@ export function createMachineCommand<
|
|||||||
actor,
|
actor,
|
||||||
commandBarConfig: recursiveCommandBarConfig,
|
commandBarConfig: recursiveCommandBarConfig,
|
||||||
onCancel,
|
onCancel,
|
||||||
|
forceDisable,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.filter((c) => c !== null) as Command<T, typeof type, S[typeof type]>[]
|
.filter((c) => c !== null) as Command<T, typeof type, S[typeof type]>[]
|
||||||
@ -105,6 +108,7 @@ export function createMachineCommand<
|
|||||||
send({ type })
|
send({ type })
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
disabled: forceDisable,
|
||||||
}
|
}
|
||||||
|
|
||||||
if (commandConfig.args) {
|
if (commandConfig.args) {
|
||||||
|
@ -459,35 +459,6 @@ export const commandBarMachine = setup({
|
|||||||
Open: {
|
Open: {
|
||||||
target: 'Selecting command',
|
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: {
|
always: {
|
||||||
@ -645,6 +616,29 @@ export const commandBarMachine = setup({
|
|||||||
target: '.Command selected',
|
target: '.Command selected',
|
||||||
actions: ['Find and select command', 'Initialize arguments to submit'],
|
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
|
||||||
|
)
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user