Proper command bar UI support for optional args (#7506)

* WIP: Add bidirectional args to point-and-click Extrude
Will eventually close #7495

* Wire up edit flow for symmetric

* Show skip true args in header in review phase

* Add bidirectionalLength

* Make currentArg always part of header

* WIP

* Add twistAng

* Proper optional args line in review

* Labels in progress button and option arg section heading

* Clean up extrude specific changes

* More UI polish

* Remove options bool icon

* Fix labels for tests

* Upgrade e2e tests to cmdBar fixtures with fixes

* More fixes

* Fixed up more tests related to sweep behavior change

* Fix nodeToEdit not having hidden: true on Shell

* Add typecheck

* WIP: footer buttons

* back to reg width

* Clean up

* Clean up

* Fix tests and remove label

* Refactor

* Fix offset plane test

* Add CommandBarDivider

* Fix step back

* Add comment

* Fix it, thanks bot

* Clean up and inline optional heading

* Little case tweak

* Update src/components/CommandBar/CommandBarReview.tsx

Co-authored-by: graphite-app[bot] <96075541+graphite-app[bot]@users.noreply.github.com>

* Rename to CommandBarHeaderFooter

---------

Co-authored-by: graphite-app[bot] <96075541+graphite-app[bot]@users.noreply.github.com>
This commit is contained in:
Pierre Jacquier
2025-06-20 12:05:20 -04:00
committed by GitHub
parent 5f2a10ec7e
commit 416d0b37a2
17 changed files with 235 additions and 195 deletions

View File

@ -307,7 +307,7 @@ test.describe('Command bar tests', () => {
)
const continueButton = page.getByRole('button', { name: 'Continue' })
const submitButton = page.getByRole('button', { name: 'Submit command' })
const submitButton = page.getByTestId('command-bar-submit')
await continueButton.click()
// Review step and argument hotkeys

View File

@ -54,9 +54,7 @@ test(
await page.keyboard.press('Enter')
// Click the checkbox
const submitButton = page.getByText('Confirm Export')
await expect(submitButton).toBeVisible()
await page.keyboard.press('Enter')
await cmdBar.submit()
// Expect it to succeed
const errorToastMessage = page.getByText(`Error while exporting`)
@ -119,9 +117,7 @@ test(
await page.keyboard.press('Enter')
// Click the checkbox
const submitButton = page.getByText('Confirm Export')
await expect(submitButton).toBeVisible()
await page.keyboard.press('Enter')
await cmdBar.submit()
// Look out for the toast message
const exportingToastMessage = page.getByText(`Exporting...`)

View File

@ -118,15 +118,11 @@ export class CmdBarFixture {
return
}
const arrowButton = this.page.getByRole('button', {
name: 'arrow right Continue',
})
const arrowButton = this.page.getByTestId('command-bar-continue')
if (await arrowButton.isVisible()) {
await arrowButton.click()
await this.continue()
} else {
await this.page
.getByRole('button', { name: 'checkmark Submit command' })
.click()
await this.submit()
}
}

View File

@ -1083,14 +1083,13 @@ openSketch = startSketchOn(XY)
cmdBar,
}) => {
// One dumb hardcoded screen pixel value
const testPoint = { x: 700, y: 150 }
const testPoint = { x: 700, y: 200 }
// TODO: replace the testPoint selection with a feature tree click once that's supported #7544
const [clickOnXzPlane] = scene.makeMouseHelpers(testPoint.x, testPoint.y)
const expectedOutput = `plane001 = offsetPlane(XZ, offset = 5)`
await homePage.goToModelingScene()
// FIXME: Since there is no KCL code loaded. We need to wait for the scene to load before we continue.
// The engine may not be connected
await page.waitForTimeout(15000)
await scene.settled(cmdBar)
await test.step(`Look for the blue of the XZ plane`, async () => {
//await scene.expectPixelColor([50, 51, 96], testPoint, 15) // FIXME
@ -1829,7 +1828,6 @@ profile002 = startProfile(sketch002, at = [0, 0])
currentArgKey: 'sketches',
currentArgValue: '',
headerArguments: {
Sectional: '',
Profiles: '',
Path: '',
},
@ -1843,7 +1841,6 @@ profile002 = startProfile(sketch002, at = [0, 0])
currentArgKey: 'path',
currentArgValue: '',
headerArguments: {
Sectional: '',
Profiles: '1 profile',
Path: '',
},
@ -1856,7 +1853,6 @@ profile002 = startProfile(sketch002, at = [0, 0])
currentArgKey: 'path',
currentArgValue: '',
headerArguments: {
Sectional: '',
Profiles: '1 profile',
Path: '',
},
@ -1869,7 +1865,6 @@ profile002 = startProfile(sketch002, at = [0, 0])
headerArguments: {
Profiles: '1 profile',
Path: '1 segment',
Sectional: '',
},
stage: 'review',
})
@ -1894,6 +1889,9 @@ profile002 = startProfile(sketch002, at = [0, 0])
0
)
await operationButton.dblclick({ button: 'left' })
await page
.getByRole('button', { name: 'sectional', exact: false })
.click()
await cmdBar.expectState({
commandName: 'Sweep',
currentArgKey: 'sectional',
@ -1971,7 +1969,6 @@ profile001 = ${circleCode}`
currentArgKey: 'sketches',
currentArgValue: '',
headerArguments: {
Sectional: '',
Profiles: '',
Path: '',
},
@ -1986,7 +1983,6 @@ profile001 = ${circleCode}`
currentArgKey: 'path',
currentArgValue: '',
headerArguments: {
Sectional: '',
Profiles: '1 profile',
Path: '',
},
@ -2000,7 +1996,6 @@ profile001 = ${circleCode}`
currentArgKey: 'path',
currentArgValue: '',
headerArguments: {
Sectional: '',
Profiles: '1 profile',
Path: '',
},
@ -2013,7 +2008,6 @@ profile001 = ${circleCode}`
headerArguments: {
Profiles: '1 profile',
Path: '1 helix',
Sectional: '',
},
stage: 'review',
})
@ -4734,7 +4728,6 @@ path001 = startProfile(sketch001, at = [0, 0])
headerArguments: {
Profiles: '',
Path: '',
Sectional: '',
},
highlightedHeaderArg: 'Profiles',
commandName: 'Sweep',
@ -4747,7 +4740,6 @@ path001 = startProfile(sketch001, at = [0, 0])
headerArguments: {
Profiles: '2 profiles',
Path: '',
Sectional: '',
},
highlightedHeaderArg: 'path',
commandName: 'Sweep',
@ -4760,7 +4752,6 @@ path001 = startProfile(sketch001, at = [0, 0])
headerArguments: {
Profiles: '2 profiles',
Path: '1 segment',
Sectional: '',
},
commandName: 'Sweep',
})

View File

@ -475,6 +475,7 @@ test.describe('Can export from electron app', () => {
},
tronApp.projectDirName,
page,
cmdBar,
method
)
)
@ -779,9 +780,6 @@ test.describe(`Project management commands`, () => {
const commandContinueButton = page.getByRole('button', {
name: 'Continue',
})
const commandSubmitButton = page.getByRole('button', {
name: 'Submit command',
})
const toastMessage = page.getByText(`Successfully renamed`)
await test.step(`Setup`, async () => {
@ -800,8 +798,7 @@ test.describe(`Project management commands`, () => {
await expect(commandContinueButton).toBeVisible()
await commandContinueButton.click()
await expect(commandSubmitButton).toBeVisible()
await commandSubmitButton.click()
await cmdBar.submit()
await expect(toastMessage).toBeVisible()
})
@ -837,9 +834,6 @@ test.describe(`Project management commands`, () => {
})
const projectNameOption = page.getByRole('option', { name: projectName })
const commandWarning = page.getByText('Are you sure you want to delete?')
const commandSubmitButton = page.getByRole('button', {
name: 'Submit command',
})
const toastMessage = page.getByText(`Successfully deleted`)
const noProjectsMessage = page.getByText('No projects found')
@ -859,8 +853,7 @@ test.describe(`Project management commands`, () => {
await projectNameOption.click()
await expect(commandWarning).toBeVisible()
await expect(commandSubmitButton).toBeVisible()
await commandSubmitButton.click()
await cmdBar.submit()
await expect(toastMessage).toBeVisible()
})
@ -894,9 +887,6 @@ test.describe(`Project management commands`, () => {
const commandContinueButton = page.getByRole('button', {
name: 'Continue',
})
const commandSubmitButton = page.getByRole('button', {
name: 'Submit command',
})
const toastMessage = page.getByText(`Successfully renamed`)
await test.step(`Setup`, async () => {
@ -914,8 +904,7 @@ test.describe(`Project management commands`, () => {
await expect(commandContinueButton).toBeVisible()
await commandContinueButton.click()
await expect(commandSubmitButton).toBeVisible()
await commandSubmitButton.click()
await cmdBar.submit()
await expect(toastMessage).toBeVisible()
})
@ -949,9 +938,6 @@ test.describe(`Project management commands`, () => {
})
const projectNameOption = page.getByRole('option', { name: projectName })
const commandWarning = page.getByText('Are you sure you want to delete?')
const commandSubmitButton = page.getByRole('button', {
name: 'Submit command',
})
const toastMessage = page.getByText(`Successfully deleted`)
const noProjectsMessage = page.getByText('No projects found')
@ -967,8 +953,7 @@ test.describe(`Project management commands`, () => {
await projectNameOption.click()
await expect(commandWarning).toBeVisible()
await expect(commandSubmitButton).toBeVisible()
await commandSubmitButton.click()
await cmdBar.submit()
await expect(toastMessage).toBeVisible()
})

View File

@ -1,6 +1,7 @@
import path from 'path'
import { bracket } from '@e2e/playwright/fixtures/bracket'
import type { Page } from '@playwright/test'
import type { CmdBarFixture } from '@e2e/playwright/fixtures/cmdBarFixture'
import { reportRejection } from '@src/lib/trap'
import * as fsp from 'fs/promises'
@ -421,10 +422,7 @@ extrude002 = extrude(profile002, length = 150)
await page.keyboard.press('Enter')
// Click the checkbox
const submitButton = page.getByText('Confirm Export')
await expect(submitButton).toBeVisible()
await page.keyboard.press('Enter')
await cmdBar.submit()
// Find the toast.
// Look out for the toast message
@ -461,8 +459,7 @@ extrude002 = extrude(profile002, length = 150)
await page.keyboard.press('Enter')
// Click the checkbox
await expect(submitButton).toBeVisible()
await page.keyboard.press('Enter')
await cmdBar.submit()
// Find the toast.
// Look out for the toast message
@ -482,6 +479,7 @@ extrude002 = extrude(profile002, length = 150)
test('ensure you CAN export while an export is already going', async ({
page,
homePage,
cmdBar,
}) => {
const u = await getUtils(page)
await test.step('Set up the code and durations', async () => {
@ -516,11 +514,11 @@ extrude002 = extrude(profile002, length = 150)
const successToastMessage = page.getByText(`Exported successfully`)
await test.step('second export', async () => {
await clickExportButton(page)
await clickExportButton(page, cmdBar)
await expect(exportingToastMessage).toBeVisible()
await clickExportButton(page)
await clickExportButton(page, cmdBar)
await test.step('The first export still succeeds', async () => {
await Promise.all([
@ -537,7 +535,7 @@ extrude002 = extrude(profile002, length = 150)
await test.step('Successful, unblocked export', async () => {
// Try exporting again.
await clickExportButton(page)
await clickExportButton(page, cmdBar)
// Find the toast.
// Look out for the toast message
@ -880,7 +878,7 @@ s2 = startSketchOn(XY)
})
})
async function clickExportButton(page: Page) {
async function clickExportButton(page: Page, cmdBar: CmdBarFixture) {
await test.step('Running export flow', async () => {
// export the model
const exportButton = page.getByTestId('export-pane-button')
@ -896,9 +894,6 @@ async function clickExportButton(page: Page) {
await page.keyboard.press('Enter')
// Click the checkbox
const submitButton = page.getByText('Confirm Export')
await expect(submitButton).toBeVisible()
await page.keyboard.press('Enter')
await cmdBar.submit()
})
}

View File

@ -22,6 +22,7 @@ export const token = process.env.token || ''
import type { ProjectConfiguration } from '@rust/kcl-lib/bindings/ProjectConfiguration'
import type { ElectronZoo } from '@e2e/playwright/fixtures/fixtureSetup'
import type { CmdBarFixture } from '@e2e/playwright/fixtures/cmdBarFixture'
import { isErrorWhitelisted } from '@e2e/playwright/lib/console-error-whitelist'
import { TEST_SETTINGS, TEST_SETTINGS_KEY } from '@e2e/playwright/storageStates'
import { test } from '@e2e/playwright/zoo-test'
@ -737,6 +738,7 @@ export const doExport = async (
output: Models['OutputFormat3d_type'],
rootDir: string,
page: Page,
cmdBar: CmdBarFixture,
exportFrom: 'dropdown' | 'sidebarButton' | 'commandBar' = 'dropdown'
): Promise<Paths> => {
if (exportFrom === 'dropdown') {
@ -780,9 +782,7 @@ export const doExport = async (
.click()
await page.locator('#arg-form').waitFor({ state: 'detached' })
}
await expect(page.getByText('Confirm Export')).toBeVisible()
await page.getByRole('button', { name: 'Submit command' }).click()
await cmdBar.submit()
await expect(page.getByText('Exported successfully')).toBeVisible()

View File

@ -10,7 +10,7 @@ import {
import { expect, test } from '@e2e/playwright/zoo-test'
test.describe('Testing constraints', () => {
test('Can constrain line length', async ({ page, homePage }) => {
test('Can constrain line length', async ({ page, homePage, cmdBar }) => {
await page.addInitScript(async () => {
localStorage.setItem(
'persistCode',
@ -50,11 +50,7 @@ test.describe('Testing constraints', () => {
await page.waitForTimeout(100)
await page.getByTestId('constraint-length').click()
await page.getByTestId('cmd-bar-arg-value').getByRole('textbox').fill('20')
await page
.getByRole('button', {
name: 'arrow right Continue',
})
.click()
await cmdBar.continue()
await expect(page.locator('.cm-content')).toHaveText(
`length001 = 20sketch001 = startSketchOn(XY) |> startProfile(at = [-10, -10]) |> line(end = [20, 0]) |> angledLine(angle = 90, length = length001) |> xLine(length = -20)`
@ -681,9 +677,6 @@ test.describe('Testing constraints', () => {
.getByRole('textbox')
const cmdBarKclVariableNameInput =
page.getByPlaceholder('Variable name')
const cmdBarSubmitButton = page.getByRole('button', {
name: 'arrow right Continue',
})
await page.addInitScript(async () => {
localStorage.setItem(
@ -736,7 +729,7 @@ part002 = startSketchOn(XZ)
await page.waitForTimeout(500)
const [ang, len] = value.split(', ')
const changedCode = `|> angledLine(angle = ${ang}, length = ${len})`
await cmdBarSubmitButton.click()
await cmdBar.continue()
await expect(page.locator('.cm-content')).toContainText(changedCode)
// checking active assures the cursor is where it should be
@ -1101,11 +1094,7 @@ part002 = startSketchOn(XZ)
await page.waitForTimeout(500)
await page.getByTestId('cmd-bar-arg-value').getByRole('textbox').fill('10')
await page
.getByRole('button', {
name: 'arrow right Continue',
})
.click()
await cmdBar.continue()
await pollEditorLinesSelectedLength(page, 1)
activeLinesContent = await page.locator('.cm-activeLine').all()

View File

@ -21,7 +21,7 @@ test.describe('Testing loading external models', () => {
// We have no more web tests
test.fail(
'Web: should overwrite current code, cannot create new file',
async ({ editor, context, page, homePage }) => {
async ({ editor, context, page, homePage, cmdBar }) => {
const u = await getUtils(page)
await test.step(`Test setup`, async () => {
await context.addInitScript((code) => {
@ -52,9 +52,6 @@ test.describe('Testing loading external models', () => {
name,
})
const warningText = page.getByText('Overwrite current file with sample?')
const confirmButton = page.getByRole('button', {
name: 'Submit command',
})
await test.step(`Precondition: check the initial code`, async () => {
await u.openKclCodePanel()
@ -70,7 +67,7 @@ test.describe('Testing loading external models', () => {
await expect(commandMethodOption('Create new file')).not.toBeVisible()
await commandMethodOption('Overwrite').click()
await expect(warningText).toBeVisible()
await confirmButton.click()
await cmdBar.submit()
await editor.expectEditor.toContain('// ' + newSample.title)
})

View File

@ -3,6 +3,7 @@ import type { LineInputsType } from '@src/lang/std/sketchcombos'
import { uuidv4 } from '@src/lib/utils'
import type { EditorFixture } from '@e2e/playwright/fixtures/editorFixture'
import type { CmdBarFixture } from '@e2e/playwright/fixtures/cmdBarFixture'
import { deg, getUtils, wiggleMove } from '@e2e/playwright/test-utils'
import { expect, test } from '@e2e/playwright/zoo-test'
@ -18,7 +19,7 @@ test.describe('Testing segment overlays', () => {
* @param {number} options.steps - The number of steps to perform
*/
const _clickConstrained =
(page: Page, editor: EditorFixture) =>
(page: Page, editor: EditorFixture, cmdBar: CmdBarFixture) =>
async ({
hoverPos,
constraintType,
@ -93,11 +94,7 @@ test.describe('Testing segment overlays', () => {
page.getByTestId('cmd-bar-arg-value').getByRole('textbox')
).toBeFocused()
await page.waitForTimeout(500)
await page
.getByRole('button', {
name: 'arrow right Continue',
})
.click()
await cmdBar.continue()
await editor.expectEditor.toContain(expectFinal, {
shouldNormalise: true,
})
@ -113,7 +110,7 @@ test.describe('Testing segment overlays', () => {
* @param {number} options.steps - The number of steps to perform
*/
const _clickUnconstrained =
(page: Page, editor: EditorFixture) =>
(page: Page, editor: EditorFixture, cmdBar: CmdBarFixture) =>
async ({
hoverPos,
constraintType,
@ -163,11 +160,7 @@ test.describe('Testing segment overlays', () => {
page.getByTestId('cmd-bar-arg-value').getByRole('textbox')
).toBeFocused()
await page.waitForTimeout(500)
await page
.getByRole('button', {
name: 'arrow right Continue',
})
.click()
await cmdBar.continue()
await editor.expectEditor.toContain(expectAfterUnconstrained, {
shouldNormalise: true,
})
@ -239,8 +232,8 @@ test.describe('Testing segment overlays', () => {
await expect(page.getByTestId('segment-overlay')).toHaveCount(14)
const clickUnconstrained = _clickUnconstrained(page, editor)
const clickConstrained = _clickConstrained(page, editor)
const clickUnconstrained = _clickUnconstrained(page, editor, cmdBar)
const clickConstrained = _clickConstrained(page, editor, cmdBar)
await u.openAndClearDebugPanel()
await u.sendCustomCmd({
@ -664,7 +657,7 @@ profile002 = circle(sketch001, center = [345, 0], radius = 238.38)
await expect(
page.getByTestId('cmd-bar-arg-value').getByRole('textbox')
).toBeFocused()
await page.getByRole('button', { name: 'arrow right Continue' }).click()
await cmdBar.continue()
// Verify the X constraint was added
await editor.expectEditor.toContain('center = [xAbs001, 0]', {
@ -682,7 +675,7 @@ profile002 = circle(sketch001, center = [345, 0], radius = 238.38)
await expect(
page.getByTestId('cmd-bar-arg-value').getByRole('textbox')
).toBeFocused()
await page.getByRole('button', { name: 'arrow right Continue' }).click()
await cmdBar.continue()
// Verify the Y constraint was added
await editor.expectEditor.toContain('center = [xAbs001, yAbs001]', {
@ -700,7 +693,7 @@ profile002 = circle(sketch001, center = [345, 0], radius = 238.38)
await expect(
page.getByTestId('cmd-bar-arg-value').getByRole('textbox')
).toBeFocused()
await page.getByRole('button', { name: 'arrow right Continue' }).click()
await cmdBar.continue()
// Verify all constraints were added
await editor.expectEditor.toContain(
@ -887,7 +880,7 @@ profile003 = startProfile(sketch001, at = [64.39, 35.16])
await expect(
page.getByTestId('cmd-bar-arg-value').getByRole('textbox')
).toBeFocused()
await page.getByRole('button', { name: 'arrow right Continue' }).click()
await cmdBar.continue()
// Verify the constraint was added
await editor.expectEditor.toContain(
@ -910,7 +903,7 @@ profile003 = startProfile(sketch001, at = [64.39, 35.16])
await expect(
page.getByTestId('cmd-bar-arg-value').getByRole('textbox')
).toBeFocused()
await page.getByRole('button', { name: 'arrow right Continue' }).click()
await cmdBar.continue()
// Verify both constraints were added
await editor.expectEditor.toContain(
@ -935,7 +928,7 @@ profile003 = startProfile(sketch001, at = [64.39, 35.16])
await expect(
page.getByTestId('cmd-bar-arg-value').getByRole('textbox')
).toBeFocused()
await page.getByRole('button', { name: 'arrow right Continue' }).click()
await cmdBar.continue()
// Verify the constraint was added
await editor.expectEditor.toContain('endAbsolute = [xAbs002, 84.07]', {
@ -955,7 +948,7 @@ profile003 = startProfile(sketch001, at = [64.39, 35.16])
await expect(
page.getByTestId('cmd-bar-arg-value').getByRole('textbox')
).toBeFocused()
await page.getByRole('button', { name: 'arrow right Continue' }).click()
await cmdBar.continue()
// Verify all constraints were added
await editor.expectEditor.toContain(

View File

@ -32,7 +32,7 @@ test('Units menu', async ({ page, homePage }) => {
test(
'Successful export shows a success toast',
{ tag: '@skipLocalEngine' },
async ({ page, homePage, tronApp }) => {
async ({ page, homePage, cmdBar, tronApp }) => {
// FYI this test doesn't work with only engine running locally
// And you will need to have the KittyCAD CLI installed
const u = await getUtils(page)
@ -94,7 +94,8 @@ part001 = startSketchOn(-XZ)
presentation: 'pretty',
},
tronApp?.projectDirName,
page
page,
cmdBar
)
}
)
@ -254,6 +255,7 @@ test('First escape in tool pops you out of tool, second exits sketch mode', asyn
test('Basic default modeling and sketch hotkeys work', async ({
page,
homePage,
cmdBar,
}) => {
const u = await getUtils(page)
await test.step(`Set up test`, async () => {
@ -397,11 +399,8 @@ test('Basic default modeling and sketch hotkeys work', async ({
await expect(page.getByRole('button', { name: 'Continue' })).toBeVisible({
timeout: 20_000,
})
await page.getByRole('button', { name: 'Continue' }).click()
await expect(
page.getByRole('button', { name: 'Submit command' })
).toBeVisible()
await page.getByRole('button', { name: 'Submit command' }).click()
await cmdBar.continue()
await cmdBar.submit()
await expect(page.locator('.cm-content')).toContainText('extrude(')
})
@ -575,8 +574,7 @@ profile001 = startProfile(sketch002, at = [-12.34, 12.34])
await cmdBar.progressCmdBar()
await cmdBar.progressCmdBar()
await expect(page.getByText('Confirm Extrude')).toBeVisible()
await cmdBar.progressCmdBar()
await cmdBar.submit()
const result2 = result.genNext`
const sketch002 = extrude(sketch002, length = ${[5, 5]} + 7)`

View File

@ -21,7 +21,7 @@ export const CommandBar = () => {
const commandBarState = useCommandBarState()
const { immediateState } = useNetworkContext()
const {
context: { selectedCommand, currentArgument, commands },
context: { selectedCommand, currentArgument, commands, argumentsToSubmit },
} = commandBarState
const isArgumentThatShouldBeHardToDismiss =
currentArgument?.inputType === 'selection' ||
@ -68,16 +68,23 @@ export const CommandBar = () => {
})
function stepBack() {
if (!currentArgument) {
if (commandBarState.matches('Review')) {
const entries = Object.entries(selectedCommand?.args || {}).filter(
([_, argConfig]) =>
!argConfig.hidden &&
(typeof argConfig.required === 'function'
? argConfig.required(commandBarState.context)
: argConfig.required)
([argName, arg]) => {
const argValue =
(typeof argumentsToSubmit[argName] === 'function'
? argumentsToSubmit[argName](commandBarState.context)
: argumentsToSubmit[argName]) || ''
const isRequired =
typeof arg.required === 'function'
? arg.required(commandBarState.context)
: arg.required
return !arg.hidden && (argValue || isRequired)
}
)
if (!currentArgument) {
if (commandBarState.matches('Review')) {
const currentArgName = entries[entries.length - 1][0]
const currentArg = {
name: currentArgName,
@ -94,9 +101,6 @@ export const CommandBar = () => {
commandBarActor.send({ type: 'Deselect command' })
}
} else {
const entries = Object.entries(selectedCommand?.args || {}).filter(
(a) => !a[1].hidden
)
const index = entries.findIndex(
([key, _]) => key === currentArgument.name
)
@ -183,20 +187,6 @@ export const CommandBar = () => {
<kbd className="hotkey ml-4 dark:!bg-chalkboard-80">esc</kbd>
</Tooltip>
</button>
{!commandBarState.matches('Selecting command') && (
<button onClick={stepBack} className="m-0 p-0 border-none">
<CustomIcon name="arrowLeft" className="w-5 h-5 rounded-sm" />
<Tooltip position="bottom">
Step back{' '}
<kbd className="hotkey ml-4 dark:!bg-chalkboard-80">
Shift
</kbd>
<kbd className="hotkey ml-4 dark:!bg-chalkboard-80">
Bksp
</kbd>
</Tooltip>
</button>
)}
</div>
</WrapperComponent.Panel>
</Transition.Child>

View File

@ -1,11 +1,12 @@
import CommandArgOptionInput from '@src/components/CommandBar/CommandArgOptionInput'
import CommandBarBasicInput from '@src/components/CommandBar/CommandBarBasicInput'
import CommandBarHeader from '@src/components/CommandBar/CommandBarHeader'
import CommandBarHeaderFooter from '@src/components/CommandBar/CommandBarHeaderFooter'
import CommandBarKclInput from '@src/components/CommandBar/CommandBarKclInput'
import CommandBarPathInput from '@src/components/CommandBar/CommandBarPathInput'
import CommandBarSelectionInput from '@src/components/CommandBar/CommandBarSelectionInput'
import CommandBarSelectionMixedInput from '@src/components/CommandBar/CommandBarSelectionMixedInput'
import CommandBarTextareaInput from '@src/components/CommandBar/CommandBarTextareaInput'
import CommandBarDivider from '@src/components/CommandBar/CommandBarDivider'
import type { CommandArgument } from '@src/lib/commandTypes'
import { commandBarActor, useCommandBarState } from '@src/lib/singletons'
@ -28,13 +29,14 @@ function CommandBarArgument({ stepBack }: { stepBack: () => void }) {
return (
currentArgument && (
<CommandBarHeader>
<CommandBarHeaderFooter stepBack={stepBack}>
<ArgumentInput
arg={currentArgument}
stepBack={stepBack}
onSubmit={onSubmit}
/>
</CommandBarHeader>
<CommandBarDivider />
</CommandBarHeaderFooter>
)
)
}

View File

@ -0,0 +1,5 @@
export default function CommandBarDivider() {
return (
<div className="block w-full my-2 h-[1px] bg-chalkboard-20 dark:bg-chalkboard-80" />
)
}

View File

@ -5,6 +5,7 @@ import { useHotkeys } from 'react-hotkeys-hook'
import { ActionButton } from '@src/components/ActionButton'
import { CustomIcon } from '@src/components/CustomIcon'
import Tooltip from '@src/components/Tooltip'
import CommandBarDivider from '@src/components/CommandBar/CommandBarDivider'
import type {
KclCommandValue,
KclExpressionWithVariable,
@ -14,7 +15,10 @@ import { getSelectionTypeDisplayText } from '@src/lib/selections'
import { roundOff } from '@src/lib/utils'
import { commandBarActor, useCommandBarState } from '@src/lib/singletons'
function CommandBarHeader({ children }: React.PropsWithChildren<object>) {
function CommandBarHeaderFooter({
children,
stepBack,
}: React.PropsWithChildren<object> & { stepBack: () => void }) {
const commandBarState = useCommandBarState()
const {
context: { selectedCommand, currentArgument, argumentsToSubmit },
@ -102,19 +106,23 @@ function CommandBarHeader({ children }: React.PropsWithChildren<object>) {
<span className="pr-2" />
)}
</p>
{Object.entries(nonHiddenArgs || {})
.filter(
([_, argConfig]) =>
argConfig.skip === false ||
(typeof argConfig.required === 'function'
? argConfig.required(commandBarState.context)
: argConfig.required)
)
.map(([argName, arg], i) => {
{Object.entries(nonHiddenArgs || {}).flatMap(
([argName, arg], i) => {
const argValue =
(typeof argumentsToSubmit[argName] === 'function'
? argumentsToSubmit[argName](commandBarState.context)
: argumentsToSubmit[argName]) || ''
const isCurrentArg = argName === currentArgument?.name
const isSkipFalse = arg.skip === false
const isRequired =
typeof arg.required === 'function'
? arg.required(commandBarState.context)
: arg.required
// We actually want to show non-hidden optional args that have a value set already
if (!(argValue || isCurrentArg || isSkipFalse || isRequired)) {
return []
}
return (
<button
@ -161,7 +169,9 @@ function CommandBarHeader({ children }: React.PropsWithChildren<object>) {
),
4
)
) : arg.inputType === 'text' && !arg.valueSummary ? (
) : arg.inputType === 'text' &&
!arg.valueSummary &&
typeof argValue === 'string' ? (
`${argValue.slice(0, 12)}${argValue.length > 12 ? '...' : ''}`
) : typeof argValue === 'object' ? (
arg.valueSummary ? (
@ -207,8 +217,14 @@ function CommandBarHeader({ children }: React.PropsWithChildren<object>) {
)}
</button>
)
})}
}
)}
</div>
</div>
<CommandBarDivider />
{children}
<div className="px-4 pb-2 flex justify-between items-center gap-2">
<StepBackButton stepBack={stepBack} />
{isReviewing ? (
<ReviewingButton
bgClassName={
@ -237,8 +253,6 @@ function CommandBarHeader({ children }: React.PropsWithChildren<object>) {
/>
)}
</div>
<div className="block w-full my-2 h-[1px] bg-chalkboard-20 dark:bg-chalkboard-80" />
{children}
</>
)
)
@ -258,16 +272,16 @@ function ReviewingButton({ bgClassName, iconClassName }: ButtonProps) {
ref={buttonRef}
type="submit"
form="review-form"
className="w-fit !p-0 rounded-sm hover:shadow focus:outline-current"
className={`w-fit !p-0 rounded-sm hover:brightness-110 hover:shadow focus:outline-current ${bgClassName}`}
tabIndex={0}
data-testid="command-bar-submit"
iconStart={{
iconEnd={{
icon: 'checkmark',
bgClassName: `p-1 rounded-sm hover:brightness-110 ${bgClassName}`,
bgClassName: `p-1 rounded-sm ${bgClassName}`,
iconClassName: `${iconClassName}`,
}}
>
<span className="sr-only">Submit command</span>
<span className={`pl-2 ${iconClassName}`}>Submit</span>
</ActionButton>
)
}
@ -278,18 +292,48 @@ function GatheringArgsButton({ bgClassName, iconClassName }: ButtonProps) {
Element="button"
type="submit"
form="arg-form"
className="w-fit !p-0 rounded-sm hover:shadow focus:outline-current"
className={`w-fit !p-0 rounded-sm hover:brightness-110 hover:shadow focus:outline-current ${bgClassName}`}
tabIndex={0}
data-testid="command-bar-continue"
iconStart={{
iconEnd={{
icon: 'arrowRight',
bgClassName: `p-1 rounded-sm hover:brightness-110 ${bgClassName}`,
bgClassName: `p-1 rounded-sm ${bgClassName}`,
iconClassName: `${iconClassName}`,
}}
>
<span className="sr-only">Continue</span>
<span className={`pl-2 ${iconClassName}`}>Continue</span>
</ActionButton>
)
}
export default CommandBarHeader
function StepBackButton({
bgClassName,
iconClassName,
stepBack,
}: ButtonProps & { stepBack: () => void }) {
return (
<ActionButton
Element="button"
type="button"
form="arg-form"
className={`w-fit !p-0 rounded-sm hover:brightness-110 hover:shadow focus:outline-current bg-chalkboard-20/50 dark:bg-chalkboard-80/50 border-chalkboard-20 dark:border-chalkboard-80 ${bgClassName}`}
tabIndex={0}
data-testid="command-bar-step-back"
iconStart={{
icon: 'arrowLeft',
bgClassName: `p-1 rounded-sm bg-chalkboard-20/50 dark:bg-chalkboard-80/50 ${bgClassName}`,
iconClassName: `${iconClassName}`,
}}
onClick={stepBack}
>
<span className={`pr-2 ${iconClassName}`}>Step back</span>
<Tooltip position="bottom">
Step back
<kbd className="hotkey ml-4 dark:!bg-chalkboard-80">Shift</kbd>
<kbd className="hotkey ml-2 dark:!bg-chalkboard-80">Bksp</kbd>
</Tooltip>
</ActionButton>
)
}
export default CommandBarHeaderFooter

View File

@ -1,7 +1,10 @@
import { useHotkeys } from 'react-hotkeys-hook'
import CommandBarHeader from '@src/components/CommandBar/CommandBarHeader'
import CommandBarHeaderFooter from '@src/components/CommandBar/CommandBarHeaderFooter'
import CommandBarDivider from '@src/components/CommandBar/CommandBarDivider'
import { commandBarActor, useCommandBarState } from '@src/lib/singletons'
import { useMemo } from 'react'
import { CustomIcon } from '@src/components/CustomIcon'
function CommandBarReview({ stepBack }: { stepBack: () => void }) {
const commandBarState = useCommandBarState()
@ -57,19 +60,71 @@ function CommandBarReview({ stepBack }: { stepBack: () => void }) {
})
}
const availableOptionalArgs = useMemo(() => {
if (!selectedCommand?.args) return undefined
const s = { ...selectedCommand.args }
for (const [name, arg] of Object.entries(s)) {
const value =
(typeof argumentsToSubmit[name] === 'function'
? argumentsToSubmit[name](commandBarState.context)
: argumentsToSubmit[name]) || ''
const isHidden =
typeof arg.hidden === 'function'
? arg.hidden(commandBarState.context)
: arg.hidden
const isRequired =
typeof arg.required === 'function'
? arg.required(commandBarState.context)
: arg.required
if (isHidden || isRequired || value) {
delete s[name]
}
}
return s
}, [selectedCommand, argumentsToSubmit, commandBarState.context])
return (
<CommandBarHeader>
<p className="px-4 pb-2">
{selectedCommand?.reviewMessage ? (
selectedCommand.reviewMessage instanceof Function ? (
selectedCommand.reviewMessage(commandBarState.context)
) : (
selectedCommand.reviewMessage
)
) : (
<>Confirm {selectedCommand?.displayName || selectedCommand?.name}</>
)}
<CommandBarHeaderFooter stepBack={stepBack}>
{selectedCommand?.reviewMessage && (
<>
<p className="px-4 py-2">
{selectedCommand.reviewMessage instanceof Function
? selectedCommand.reviewMessage(commandBarState.context)
: selectedCommand.reviewMessage}
</p>
<CommandBarDivider />
</>
)}
{Object.entries(availableOptionalArgs || {}).length > 0 && (
<>
<div className="px-4 flex flex-wrap gap-2 items-baseline">
<span className="text-sm mr-4">Optional</span>
{Object.entries(availableOptionalArgs || {}).map(
([argName, arg]) => {
return (
<button
data-testid="cmd-bar-add-optional-arg"
type="button"
onClick={() => {
commandBarActor.send({
type: 'Edit argument',
data: { arg: { ...arg, name: argName } },
})
}}
key={argName}
className="w-fit px-2 py-1 m-0 rounded-sm flex gap-2 items-center border"
>
<span className="capitalize">
{arg.displayName || argName}
</span>
<CustomIcon name="plus" className="w-4 h-4" />
</button>
)
}
)}
</div>
<CommandBarDivider />
</>
)}
<form
id="review-form"
className="absolute opacity-0 inset-0 pointer-events-none"
@ -97,7 +152,7 @@ function CommandBarReview({ stepBack }: { stepBack: () => void }) {
)
})}
</form>
</CommandBarHeader>
</CommandBarHeaderFooter>
)
}

View File

@ -275,6 +275,10 @@ export const modelingMachineCommandConfig: StateMachineCommandSetConfig<
['gltf', 'stl', 'ply'].includes(
commandContext.argumentsToSubmit.type as string
),
hidden: (commandContext) =>
!['gltf', 'stl', 'ply'].includes(
commandContext.argumentsToSubmit.type as string
),
options: (commandContext) => {
const type = commandContext.argumentsToSubmit.type as
| OutputTypeKey
@ -428,9 +432,7 @@ export const modelingMachineCommandConfig: StateMachineCommandSetConfig<
sectional: {
inputType: 'options',
skip: true,
defaultValue: false,
hidden: false,
required: true,
required: false,
options: [
{ name: 'False', value: false },
{ name: 'True', value: true },
@ -464,6 +466,7 @@ export const modelingMachineCommandConfig: StateMachineCommandSetConfig<
skip: true,
inputType: 'text',
required: false,
hidden: true,
},
sketches: {
inputType: 'selection',
@ -484,27 +487,27 @@ export const modelingMachineCommandConfig: StateMachineCommandSetConfig<
hidden: (context) => Boolean(context.argumentsToSubmit.nodeToEdit),
},
axis: {
required: (commandContext) =>
['Axis'].includes(
commandContext.argumentsToSubmit.axisOrEdge as string
),
required: (context) =>
['Axis'].includes(context.argumentsToSubmit.axisOrEdge as string),
inputType: 'options',
displayName: 'Sketch Axis',
options: [
{ name: 'X Axis', isCurrent: true, value: 'X' },
{ name: 'Y Axis', isCurrent: false, value: 'Y' },
],
hidden: (context) => Boolean(context.argumentsToSubmit.nodeToEdit),
hidden: (context) =>
Boolean(context.argumentsToSubmit.nodeToEdit) ||
!['Axis'].includes(context.argumentsToSubmit.axisOrEdge as string),
},
edge: {
required: (commandContext) =>
['Edge'].includes(
commandContext.argumentsToSubmit.axisOrEdge as string
),
required: (context) =>
['Edge'].includes(context.argumentsToSubmit.axisOrEdge as string),
inputType: 'selection',
selectionTypes: ['segment', 'sweepEdge', 'edgeCutEdge'],
multiple: false,
hidden: (context) => Boolean(context.argumentsToSubmit.nodeToEdit),
hidden: (context) =>
Boolean(context.argumentsToSubmit.nodeToEdit) ||
!['Edge'].includes(context.argumentsToSubmit.axisOrEdge as string),
},
angle: {
inputType: 'kcl',
@ -524,6 +527,7 @@ export const modelingMachineCommandConfig: StateMachineCommandSetConfig<
skip: true,
inputType: 'text',
required: false,
hidden: true,
},
selection: {
inputType: 'selection',