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:
@ -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 () => {
|
||||||
|
@ -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({
|
||||||
|
@ -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
|
||||||
)
|
)
|
||||||
|
@ -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)
|
||||||
|
@ -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',
|
||||||
|
@ -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
|
||||||
|
@ -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,
|
||||||
|
@ -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,
|
||||||
|
Reference in New Issue
Block a user