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> => {
if (!(await this.page.getByTestId('command-bar-wrapper').isVisible())) {
return { stage: 'commandBarClosed' }
} else if (await this.page.getByTestId('cmd-bar-search').isVisible()) {
return { stage: 'pickCommand' }
}
const reviewForm = this.page.locator('#review-form')
const getHeaderArgs = async () => {

View File

@ -2877,23 +2877,41 @@ extrude001 = extrude(profile001, length = 100)
shapeColor: [number, number, number]
) {
await toolbar.openPane('feature-tree')
const operationButton = await toolbar.getFeatureTreeOperation(
'Extrude',
0
)
await operationButton.click({ button: 'right' })
const menuButton = page.getByTestId('context-menu-set-appearance')
await menuButton.click()
await cmdBar.expectState({
commandName: 'Appearance',
currentArgKey: 'color',
currentArgValue: '',
headerArguments: {
Color: '',
},
highlightedHeaderArg: 'color',
stage: 'arguments',
const enterAppearanceFlow = async (stepName: string) =>
test.step(stepName, async () => {
const operationButton = await toolbar.getFeatureTreeOperation(
'Extrude',
0
)
await operationButton.click({ button: 'right' })
const menuButton = page.getByTestId('context-menu-set-appearance')
await menuButton.click()
await cmdBar.expectState({
commandName: 'Appearance',
currentArgKey: 'color',
currentArgValue: '',
headerArguments: {
Color: '',
},
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 })
await item.click()
await cmdBar.expectState({

View File

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

View File

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

View File

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

View File

@ -119,6 +119,8 @@ export type CommandArgumentConfig<
machineContext?: C
) => boolean)
warningMessage?: string
/** If `true`, arg is used as passed-through data, never for user input */
hidden?: boolean
skip?: boolean
/** For showing a summary display of the current value, such as in
* 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
machineContext?: ContextFrom<T>
) => boolean)
/** If `true`, arg is used as passed-through data, never for user input */
hidden?: boolean
skip?: boolean
machineActor?: Actor<T>
warningMessage?: string

View File

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

View File

@ -132,14 +132,15 @@ export const commandBarMachine = setup({
// Find the first argument that is not to be skipped:
// 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
const nonHiddenArgs = Object.entries(selectedCommand.args).filter(
(a) => !a[1].hidden
)
let argIndex = 0
while (argIndex < Object.keys(selectedCommand.args).length) {
const [argName, argConfig] = Object.entries(selectedCommand.args)[
argIndex
]
while (argIndex < nonHiddenArgs.length) {
const [argName, argConfig] = nonHiddenArgs[argIndex]
const argIsRequired =
typeof argConfig.required === 'function'
? argConfig.required(context)
@ -155,7 +156,7 @@ export const commandBarMachine = setup({
if (
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,
// return the last argument.
@ -259,7 +260,7 @@ export const commandBarMachine = setup({
},
'All arguments are skippable': ({ context }) => {
return Object.values(context.selectedCommand!.args!).every(
(argConfig) => argConfig.skip
(argConfig) => argConfig.skip || argConfig.hidden
)
},
'Has selected command': ({ context }) => !!context.selectedCommand,