Add support for hidden command arguments (#5534)

* Add configuration/type support for `hidden`

* Add UI support for `hidden` configuration

* Make `nodeToEdit` args hidden

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* Fix cmdBarFixture to actually serialize to "pickCommand" case

* Add test step to ensure hidden commands can't be backed into

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
This commit is contained in:
Frank Noirot
2025-02-28 18:00:29 -05:00
committed by GitHub
parent 8403025c77
commit 466c23a9d8
8 changed files with 72 additions and 33 deletions

View File

@ -47,6 +47,8 @@ export class CmdBarFixture {
private _serialiseCmdBar = async (): Promise<CmdBarSerialised> => { private _serialiseCmdBar = async (): Promise<CmdBarSerialised> => {
if (!(await this.page.getByTestId('command-bar-wrapper').isVisible())) { if (!(await this.page.getByTestId('command-bar-wrapper').isVisible())) {
return { stage: 'commandBarClosed' } return { stage: 'commandBarClosed' }
} else if (await this.page.getByTestId('cmd-bar-search').isVisible()) {
return { stage: 'pickCommand' }
} }
const reviewForm = this.page.locator('#review-form') const reviewForm = this.page.locator('#review-form')
const getHeaderArgs = async () => { const getHeaderArgs = async () => {

View File

@ -2877,23 +2877,41 @@ extrude001 = extrude(profile001, length = 100)
shapeColor: [number, number, number] shapeColor: [number, number, number]
) { ) {
await toolbar.openPane('feature-tree') await toolbar.openPane('feature-tree')
const operationButton = await toolbar.getFeatureTreeOperation( const enterAppearanceFlow = async (stepName: string) =>
'Extrude', test.step(stepName, async () => {
0 const operationButton = await toolbar.getFeatureTreeOperation(
) 'Extrude',
await operationButton.click({ button: 'right' }) 0
const menuButton = page.getByTestId('context-menu-set-appearance') )
await menuButton.click() await operationButton.click({ button: 'right' })
await cmdBar.expectState({ const menuButton = page.getByTestId('context-menu-set-appearance')
commandName: 'Appearance', await menuButton.click()
currentArgKey: 'color', await cmdBar.expectState({
currentArgValue: '', commandName: 'Appearance',
headerArguments: { currentArgKey: 'color',
Color: '', currentArgValue: '',
}, headerArguments: {
highlightedHeaderArg: 'color', Color: '',
stage: 'arguments', },
highlightedHeaderArg: 'color',
stage: 'arguments',
})
})
await enterAppearanceFlow(`Open Set Appearance flow`)
await test.step(`Validate hidden argument "nodeToEdit" can't be reached with Backspace`, async () => {
await page.keyboard.press('Backspace')
await cmdBar.expectState({
stage: 'pickCommand',
})
await page.keyboard.press('Escape')
await cmdBar.expectState({
stage: 'commandBarClosed',
})
}) })
await enterAppearanceFlow(`Restart Appearance flow`)
const item = page.getByText(option, { exact: true }) const item = page.getByText(option, { exact: true })
await item.click() await item.click()
await cmdBar.expectState({ await cmdBar.expectState({

View File

@ -43,9 +43,10 @@ export const CommandBar = () => {
if (commandBarState.matches('Review')) { if (commandBarState.matches('Review')) {
const entries = Object.entries(selectedCommand?.args || {}).filter( const entries = Object.entries(selectedCommand?.args || {}).filter(
([_, argConfig]) => ([_, argConfig]) =>
typeof argConfig.required === 'function' !argConfig.hidden &&
(typeof argConfig.required === 'function'
? argConfig.required(commandBarState.context) ? argConfig.required(commandBarState.context)
: argConfig.required : argConfig.required)
) )
const currentArgName = entries[entries.length - 1][0] const currentArgName = entries[entries.length - 1][0]
@ -64,7 +65,9 @@ export const CommandBar = () => {
commandBarActor.send({ type: 'Deselect command' }) commandBarActor.send({ type: 'Deselect command' })
} }
} else { } else {
const entries = Object.entries(selectedCommand?.args || {}) const entries = Object.entries(selectedCommand?.args || {}).filter(
(a) => !a[1].hidden
)
const index = entries.findIndex( const index = entries.findIndex(
([key, _]) => key === currentArgument.name ([key, _]) => key === currentArgument.name
) )

View File

@ -1,5 +1,5 @@
import { CustomIcon } from '../CustomIcon' import { CustomIcon } from '../CustomIcon'
import React, { useState } from 'react' import React, { useMemo, useState } from 'react'
import { ActionButton } from '../ActionButton' import { ActionButton } from '../ActionButton'
import { Selections, getSelectionTypeDisplayText } from 'lib/selections' import { Selections, getSelectionTypeDisplayText } from 'lib/selections'
import { useHotkeys } from 'react-hotkeys-hook' import { useHotkeys } from 'react-hotkeys-hook'
@ -13,6 +13,14 @@ function CommandBarHeader({ children }: React.PropsWithChildren<{}>) {
const { const {
context: { selectedCommand, currentArgument, argumentsToSubmit }, context: { selectedCommand, currentArgument, argumentsToSubmit },
} = commandBarState } = commandBarState
const nonHiddenArgs = useMemo(() => {
if (!selectedCommand?.args) return undefined
const s = { ...selectedCommand.args }
for (const [name, arg] of Object.entries(s)) {
if (arg.hidden) delete s[name]
}
return s
}, [selectedCommand])
const isReviewing = commandBarState.matches('Review') const isReviewing = commandBarState.matches('Review')
const [showShortcuts, setShowShortcuts] = useState(false) const [showShortcuts, setShowShortcuts] = useState(false)
@ -43,11 +51,9 @@ function CommandBarHeader({ children }: React.PropsWithChildren<{}>) {
], ],
(_, b) => { (_, b) => {
if (b.keys && !Number.isNaN(parseInt(b.keys[0], 10))) { if (b.keys && !Number.isNaN(parseInt(b.keys[0], 10))) {
if (!selectedCommand?.args) return if (!nonHiddenArgs) return
const argName = Object.keys(selectedCommand.args)[ const argName = Object.keys(nonHiddenArgs)[parseInt(b.keys[0], 10) - 1]
parseInt(b.keys[0], 10) - 1 const arg = nonHiddenArgs[argName]
]
const arg = selectedCommand?.args[argName]
if (!argName || !arg) return if (!argName || !arg) return
commandBarActor.send({ commandBarActor.send({
type: 'Change current argument', type: 'Change current argument',
@ -78,7 +84,7 @@ function CommandBarHeader({ children }: React.PropsWithChildren<{}>) {
{selectedCommand.displayName || selectedCommand.name} {selectedCommand.displayName || selectedCommand.name}
</span> </span>
</p> </p>
{Object.entries(selectedCommand?.args || {}) {Object.entries(nonHiddenArgs || {})
.filter(([_, argConfig]) => .filter(([_, argConfig]) =>
typeof argConfig.required === 'function' typeof argConfig.required === 'function'
? argConfig.required(commandBarState.context) ? argConfig.required(commandBarState.context)

View File

@ -311,6 +311,7 @@ export const modelingMachineCommandConfig: StateMachineCommandSetConfig<
skip: true, skip: true,
inputType: 'text', inputType: 'text',
required: false, required: false,
hidden: true,
}, },
selection: { selection: {
inputType: 'selection', inputType: 'selection',
@ -454,6 +455,7 @@ export const modelingMachineCommandConfig: StateMachineCommandSetConfig<
skip: true, skip: true,
inputType: 'text', inputType: 'text',
required: false, required: false,
hidden: true,
}, },
plane: { plane: {
inputType: 'selection', inputType: 'selection',
@ -481,6 +483,7 @@ export const modelingMachineCommandConfig: StateMachineCommandSetConfig<
skip: true, skip: true,
inputType: 'text', inputType: 'text',
required: false, required: false,
hidden: true,
}, },
revolutions: { revolutions: {
inputType: 'kcl', inputType: 'kcl',
@ -702,6 +705,7 @@ export const modelingMachineCommandConfig: StateMachineCommandSetConfig<
skip: true, skip: true,
inputType: 'text', inputType: 'text',
required: false, required: false,
hidden: true,
}, },
color: { color: {
inputType: 'options', inputType: 'options',

View File

@ -119,6 +119,8 @@ export type CommandArgumentConfig<
machineContext?: C machineContext?: C
) => boolean) ) => boolean)
warningMessage?: string warningMessage?: string
/** If `true`, arg is used as passed-through data, never for user input */
hidden?: boolean
skip?: boolean skip?: boolean
/** For showing a summary display of the current value, such as in /** For showing a summary display of the current value, such as in
* the command bar's header * the command bar's header
@ -233,6 +235,8 @@ export type CommandArgument<
commandBarContext: { argumentsToSubmit: Record<string, unknown> }, // Should be the commandbarMachine's context, but it creates a circular dependency commandBarContext: { argumentsToSubmit: Record<string, unknown> }, // Should be the commandbarMachine's context, but it creates a circular dependency
machineContext?: ContextFrom<T> machineContext?: ContextFrom<T>
) => boolean) ) => boolean)
/** If `true`, arg is used as passed-through data, never for user input */
hidden?: boolean
skip?: boolean skip?: boolean
machineActor?: Actor<T> machineActor?: Actor<T>
warningMessage?: string warningMessage?: string

View File

@ -162,6 +162,7 @@ export function buildCommandArgument<
const baseCommandArgument = { const baseCommandArgument = {
description: arg.description, description: arg.description,
required: arg.required, required: arg.required,
hidden: arg.hidden,
skip: arg.skip, skip: arg.skip,
machineActor, machineActor,
valueSummary: arg.valueSummary, valueSummary: arg.valueSummary,

View File

@ -132,14 +132,15 @@ export const commandBarMachine = setup({
// Find the first argument that is not to be skipped: // Find the first argument that is not to be skipped:
// that is, the first argument that is not already in the argumentsToSubmit // that is, the first argument that is not already in the argumentsToSubmit
// or that is not undefined, or that is not marked as "skippable". // or hidden, or that is not undefined, or that is not marked as "skippable".
// TODO validate the type of the existing arguments // TODO validate the type of the existing arguments
const nonHiddenArgs = Object.entries(selectedCommand.args).filter(
(a) => !a[1].hidden
)
let argIndex = 0 let argIndex = 0
while (argIndex < Object.keys(selectedCommand.args).length) { while (argIndex < nonHiddenArgs.length) {
const [argName, argConfig] = Object.entries(selectedCommand.args)[ const [argName, argConfig] = nonHiddenArgs[argIndex]
argIndex
]
const argIsRequired = const argIsRequired =
typeof argConfig.required === 'function' typeof argConfig.required === 'function'
? argConfig.required(context) ? argConfig.required(context)
@ -155,7 +156,7 @@ export const commandBarMachine = setup({
if ( if (
mustNotSkipArg === true || mustNotSkipArg === true ||
argIndex + 1 === Object.keys(selectedCommand.args).length argIndex + 1 === Object.keys(nonHiddenArgs).length
) { ) {
// If we have reached the end of the arguments and none are skippable, // If we have reached the end of the arguments and none are skippable,
// return the last argument. // return the last argument.
@ -259,7 +260,7 @@ export const commandBarMachine = setup({
}, },
'All arguments are skippable': ({ context }) => { 'All arguments are skippable': ({ context }) => {
return Object.values(context.selectedCommand!.args!).every( return Object.values(context.selectedCommand!.args!).every(
(argConfig) => argConfig.skip (argConfig) => argConfig.skip || argConfig.hidden
) )
}, },
'Has selected command': ({ context }) => !!context.selectedCommand, 'Has selected command': ({ context }) => !!context.selectedCommand,