Add point-and-click Insert from local project files (#6129)
* WIP: Add point-and-click Import for geometry Will eventually fix #6120 Right now the whole loop is there but the codemod doesn't work yet * Better pathToNOde, log on non-working cm dispatch call * Add workaround to updateModelingState not working * Back to updateModelingState with a skip flag * Better todo * Change working from Import to Insert, cleanups * Sister command in kclCommands to populate file options * Improve path selector * Unsure: move importAstMod to kclCommands onSubmit 😶 * Add e2e test * Clean up for review * Add native file menu entry and test * No await yo lint said so * @lrev-Dev's suggestion to remove a comment Co-authored-by: Kurt Hutten <k.hutten@protonmail.ch> * Update to scene.settled(cmdBar) * Lint --------- Co-authored-by: Kurt Hutten <k.hutten@protonmail.ch>
This commit is contained in:
@ -44,6 +44,7 @@ export class ToolbarFixture {
|
||||
featureTreePane!: Locator
|
||||
gizmo!: Locator
|
||||
gizmoDisabled!: Locator
|
||||
insertButton!: Locator
|
||||
|
||||
constructor(page: Page) {
|
||||
this.page = page
|
||||
@ -78,6 +79,8 @@ export class ToolbarFixture {
|
||||
// element or two different elements can represent these states.
|
||||
this.gizmo = page.getByTestId('gizmo')
|
||||
this.gizmoDisabled = page.getByTestId('gizmo-disabled')
|
||||
|
||||
this.insertButton = page.getByTestId('insert-pane-button')
|
||||
}
|
||||
|
||||
get logoLink() {
|
||||
|
@ -570,6 +570,43 @@ test.describe('Native file menu', { tag: ['@electron'] }, () => {
|
||||
const expected = 'Open sample'
|
||||
expect(actual).toBe(expected)
|
||||
})
|
||||
test('Modeling.File.Insert from project file', async ({
|
||||
tronApp,
|
||||
cmdBar,
|
||||
page,
|
||||
homePage,
|
||||
scene,
|
||||
}) => {
|
||||
if (!tronApp) {
|
||||
throwTronAppMissing()
|
||||
return
|
||||
}
|
||||
await homePage.goToModelingScene()
|
||||
await scene.settled(cmdBar)
|
||||
|
||||
// Run electron snippet to find the Menu!
|
||||
await page.waitForTimeout(100) // wait for createModelingPageMenu() to run
|
||||
await tronApp.electron.evaluate(async ({ app }) => {
|
||||
if (!app || !app.applicationMenu) {
|
||||
throw new Error('app or app.applicationMenu is missing')
|
||||
}
|
||||
const openProject = app.applicationMenu.getMenuItemById(
|
||||
'File.Insert from project file'
|
||||
)
|
||||
if (!openProject) {
|
||||
throw new Error('File.Insert from project file')
|
||||
}
|
||||
openProject.click()
|
||||
})
|
||||
// Check that the command bar is opened
|
||||
await expect(cmdBar.cmdBarElement).toBeVisible()
|
||||
// Check the placeholder project name exists
|
||||
const actual = await cmdBar.cmdBarElement
|
||||
.getByTestId('command-name')
|
||||
.textContent()
|
||||
const expected = 'Insert'
|
||||
expect(actual).toBe(expected)
|
||||
})
|
||||
test('Modeling.File.Export current part', async ({
|
||||
tronApp,
|
||||
cmdBar,
|
||||
|
115
e2e/playwright/point-click-assemblies.spec.ts
Normal file
115
e2e/playwright/point-click-assemblies.spec.ts
Normal file
@ -0,0 +1,115 @@
|
||||
import * as fsp from 'fs/promises'
|
||||
import path from 'path'
|
||||
|
||||
import { executorInputPath } from '@e2e/playwright/test-utils'
|
||||
import { test } from '@e2e/playwright/zoo-test'
|
||||
|
||||
// test file is for testing point an click code gen functionality that's assemblies related
|
||||
test.describe('Point-and-click assemblies tests', () => {
|
||||
test(
|
||||
`Insert kcl part into assembly as whole module import`,
|
||||
{ tag: ['@electron'] },
|
||||
async ({
|
||||
context,
|
||||
page,
|
||||
homePage,
|
||||
scene,
|
||||
editor,
|
||||
toolbar,
|
||||
cmdBar,
|
||||
tronApp,
|
||||
}) => {
|
||||
if (!tronApp) {
|
||||
fail()
|
||||
}
|
||||
|
||||
// One dumb hardcoded screen pixel value
|
||||
const testPoint = { x: 575, y: 200 }
|
||||
const initialColor: [number, number, number] = [50, 50, 50]
|
||||
const partColor: [number, number, number] = [150, 150, 150]
|
||||
const tolerance = 50
|
||||
|
||||
await test.step('Setup parts and expect empty assembly scene', async () => {
|
||||
const projectName = 'assembly'
|
||||
await context.folderSetupFn(async (dir) => {
|
||||
const bracketDir = path.join(dir, projectName)
|
||||
await fsp.mkdir(bracketDir, { recursive: true })
|
||||
await Promise.all([
|
||||
fsp.copyFile(
|
||||
executorInputPath('cylinder-inches.kcl'),
|
||||
path.join(bracketDir, 'cylinder.kcl')
|
||||
),
|
||||
fsp.copyFile(
|
||||
executorInputPath('e2e-can-sketch-on-chamfer.kcl'),
|
||||
path.join(bracketDir, 'bracket.kcl')
|
||||
),
|
||||
fsp.writeFile(path.join(bracketDir, 'main.kcl'), ''),
|
||||
])
|
||||
})
|
||||
await page.setBodyDimensions({ width: 1000, height: 500 })
|
||||
await homePage.openProject(projectName)
|
||||
await scene.settled(cmdBar)
|
||||
await scene.expectPixelColor(initialColor, testPoint, tolerance)
|
||||
})
|
||||
|
||||
await test.step('Insert first part into the assembly', async () => {
|
||||
await toolbar.insertButton.click()
|
||||
await cmdBar.selectOption({ name: 'cylinder.kcl' }).click()
|
||||
await cmdBar.expectState({
|
||||
stage: 'arguments',
|
||||
currentArgKey: 'localName',
|
||||
currentArgValue: '',
|
||||
headerArguments: { Path: 'cylinder.kcl', LocalName: '' },
|
||||
highlightedHeaderArg: 'localName',
|
||||
commandName: 'Insert',
|
||||
})
|
||||
await page.keyboard.insertText('cylinder')
|
||||
await cmdBar.progressCmdBar()
|
||||
await cmdBar.expectState({
|
||||
stage: 'review',
|
||||
headerArguments: { Path: 'cylinder.kcl', LocalName: 'cylinder' },
|
||||
commandName: 'Insert',
|
||||
})
|
||||
await cmdBar.progressCmdBar()
|
||||
await editor.expectEditor.toContain(
|
||||
`
|
||||
import "cylinder.kcl" as cylinder
|
||||
cylinder
|
||||
`,
|
||||
{ shouldNormalise: true }
|
||||
)
|
||||
await scene.expectPixelColor(partColor, testPoint, tolerance)
|
||||
})
|
||||
|
||||
await test.step('Insert second part into the assembly', async () => {
|
||||
await toolbar.insertButton.click()
|
||||
await cmdBar.selectOption({ name: 'bracket.kcl' }).click()
|
||||
await cmdBar.expectState({
|
||||
stage: 'arguments',
|
||||
currentArgKey: 'localName',
|
||||
currentArgValue: '',
|
||||
headerArguments: { Path: 'bracket.kcl', LocalName: '' },
|
||||
highlightedHeaderArg: 'localName',
|
||||
commandName: 'Insert',
|
||||
})
|
||||
await page.keyboard.insertText('bracket')
|
||||
await cmdBar.progressCmdBar()
|
||||
await cmdBar.expectState({
|
||||
stage: 'review',
|
||||
headerArguments: { Path: 'bracket.kcl', LocalName: 'bracket' },
|
||||
commandName: 'Insert',
|
||||
})
|
||||
await cmdBar.progressCmdBar()
|
||||
await editor.expectEditor.toContain(
|
||||
`
|
||||
import "cylinder.kcl" as cylinder
|
||||
import "bracket.kcl" as bracket
|
||||
cylinder
|
||||
bracket
|
||||
`,
|
||||
{ shouldNormalise: true }
|
||||
)
|
||||
})
|
||||
}
|
||||
)
|
||||
})
|
@ -459,10 +459,30 @@ export const FileMachineProvider = ({
|
||||
name: sample.title,
|
||||
})),
|
||||
},
|
||||
specialPropsForInsertCommand: {
|
||||
providedOptions: (isDesktop() && project?.children
|
||||
? project.children
|
||||
: []
|
||||
).flatMap((v) => {
|
||||
// TODO: add support for full tree traversal when KCL support subdir imports
|
||||
const relativeFilePath = v.path.replace(
|
||||
project?.path + window.electron.sep,
|
||||
''
|
||||
)
|
||||
const isDirectory = v.children
|
||||
const isCurrentFile = v.path === file?.path
|
||||
return isDirectory || isCurrentFile
|
||||
? []
|
||||
: {
|
||||
name: relativeFilePath,
|
||||
value: relativeFilePath,
|
||||
}
|
||||
}),
|
||||
},
|
||||
}).filter(
|
||||
(command) => kclSamples.length || command.name !== 'open-kcl-example'
|
||||
),
|
||||
[codeManager, kclManager, send, kclSamples]
|
||||
[codeManager, kclManager, send, kclSamples, project, file]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -14,6 +14,7 @@ import type {
|
||||
} from '@src/components/ModelingSidebar/ModelingPanes'
|
||||
import { sidebarPanes } from '@src/components/ModelingSidebar/ModelingPanes'
|
||||
import Tooltip from '@src/components/Tooltip'
|
||||
import { DEV } from '@src/env'
|
||||
import { useModelingContext } from '@src/hooks/useModelingContext'
|
||||
import { useKclContext } from '@src/lang/KclProvider'
|
||||
import { SIDEBAR_BUTTON_SUFFIX } from '@src/lib/constants'
|
||||
@ -21,6 +22,7 @@ import { isDesktop } from '@src/lib/isDesktop'
|
||||
import { useSettings } from '@src/machines/appMachine'
|
||||
import { commandBarActor } from '@src/machines/commandBarMachine'
|
||||
import { onboardingPaths } from '@src/routes/Onboarding/paths'
|
||||
import { IS_NIGHTLY_OR_DEBUG } from '@src/routes/utils'
|
||||
|
||||
interface ModelingSidebarProps {
|
||||
paneOpacity: '' | 'opacity-20' | 'opacity-40'
|
||||
@ -60,6 +62,19 @@ export function ModelingSidebar({ paneOpacity }: ModelingSidebarProps) {
|
||||
)
|
||||
|
||||
const sidebarActions: SidebarAction[] = [
|
||||
{
|
||||
id: 'insert',
|
||||
title: 'Insert from project file',
|
||||
sidebarName: 'Insert from project file',
|
||||
icon: 'import',
|
||||
keybinding: 'Ctrl + Shift + I',
|
||||
hide: (a) => a.platform === 'web' || !(DEV || IS_NIGHTLY_OR_DEBUG),
|
||||
action: () =>
|
||||
commandBarActor.send({
|
||||
type: 'Find and select command',
|
||||
data: { name: 'Insert', groupId: 'code' },
|
||||
}),
|
||||
},
|
||||
{
|
||||
id: 'export',
|
||||
title: 'Export part',
|
||||
|
@ -113,6 +113,7 @@ function ProjectMenuPopover({
|
||||
const commands = useSelector(commandBarActor, commandsSelector)
|
||||
|
||||
const { onProjectClose } = useLspContext()
|
||||
const insertCommandInfo = { name: 'Insert', groupId: 'code' }
|
||||
const exportCommandInfo = { name: 'Export', groupId: 'modeling' }
|
||||
const makeCommandInfo = { name: 'Make', groupId: 'modeling' }
|
||||
const shareCommandInfo = { name: 'share-file-link', groupId: 'code' }
|
||||
@ -145,6 +146,29 @@ function ProjectMenuPopover({
|
||||
},
|
||||
},
|
||||
'break',
|
||||
{
|
||||
id: 'insert',
|
||||
Element: 'button',
|
||||
children: (
|
||||
<>
|
||||
<span>Insert from project file</span>
|
||||
{!findCommand(insertCommandInfo) && (
|
||||
<Tooltip
|
||||
position="right"
|
||||
wrapperClassName="!max-w-none min-w-fit"
|
||||
>
|
||||
Awaiting engine connection
|
||||
</Tooltip>
|
||||
)}
|
||||
</>
|
||||
),
|
||||
disabled: !findCommand(insertCommandInfo),
|
||||
onClick: () =>
|
||||
commandBarActor.send({
|
||||
type: 'Find and select command',
|
||||
data: insertCommandInfo,
|
||||
}),
|
||||
},
|
||||
{
|
||||
id: 'export',
|
||||
Element: 'button',
|
||||
|
@ -1,7 +1,11 @@
|
||||
import type { ImportStatement } from '@rust/kcl-lib/bindings/ImportStatement'
|
||||
import type { Name } from '@rust/kcl-lib/bindings/Name'
|
||||
import type { Node } from '@rust/kcl-lib/bindings/Node'
|
||||
import type { TagDeclarator } from '@rust/kcl-lib/bindings/TagDeclarator'
|
||||
|
||||
import type { ImportPath } from '@rust/kcl-lib/bindings/ImportPath'
|
||||
import type { ImportSelector } from '@rust/kcl-lib/bindings/ImportSelector'
|
||||
import type { ItemVisibility } from '@rust/kcl-lib/bindings/ItemVisibility'
|
||||
import { ARG_TAG } from '@src/lang/constants'
|
||||
import { getNodeFromPath } from '@src/lang/queryAst'
|
||||
import { getNodePathFromSourceRange } from '@src/lang/queryAstNodePathUtils'
|
||||
@ -12,6 +16,7 @@ import type {
|
||||
CallExpression,
|
||||
CallExpressionKw,
|
||||
Expr,
|
||||
ExpressionStatement,
|
||||
Identifier,
|
||||
LabeledArg,
|
||||
Literal,
|
||||
@ -333,6 +338,44 @@ export function createBinaryExpressionWithUnary([left, right]: [
|
||||
return createBinaryExpression([left, '+', right])
|
||||
}
|
||||
|
||||
export function createImportAsSelector(name: string): ImportSelector {
|
||||
return { type: 'None', alias: createIdentifier(name) }
|
||||
}
|
||||
|
||||
export function createImportStatement(
|
||||
selector: ImportSelector,
|
||||
path: ImportPath,
|
||||
visibility: ItemVisibility = 'default'
|
||||
): Node<ImportStatement> {
|
||||
return {
|
||||
type: 'ImportStatement',
|
||||
start: 0,
|
||||
end: 0,
|
||||
moduleId: 0,
|
||||
outerAttrs: [],
|
||||
preComments: [],
|
||||
commentStart: 0,
|
||||
selector,
|
||||
path,
|
||||
visibility,
|
||||
}
|
||||
}
|
||||
|
||||
export function createExpressionStatement(
|
||||
expression: Expr
|
||||
): Node<ExpressionStatement> {
|
||||
return {
|
||||
type: 'ExpressionStatement',
|
||||
start: 0,
|
||||
end: 0,
|
||||
moduleId: 0,
|
||||
outerAttrs: [],
|
||||
preComments: [],
|
||||
commentStart: 0,
|
||||
expression,
|
||||
}
|
||||
}
|
||||
|
||||
export function findUniqueName(
|
||||
ast: Program | string,
|
||||
name: string,
|
||||
|
@ -17,6 +17,7 @@ import {
|
||||
EXECUTION_TYPE_NONE,
|
||||
EXECUTION_TYPE_REAL,
|
||||
} from '@src/lib/constants'
|
||||
import type { Selections } from '@src/lib/selections'
|
||||
|
||||
/**
|
||||
* Updates the complete modeling state:
|
||||
@ -52,15 +53,23 @@ export async function updateModelingState(
|
||||
},
|
||||
options?: {
|
||||
focusPath?: Array<PathToNode>
|
||||
skipUpdateAst?: boolean
|
||||
}
|
||||
): Promise<void> {
|
||||
let updatedAst: {
|
||||
newAst: Node<Program>
|
||||
selections?: Selections
|
||||
} = { newAst: ast }
|
||||
// TODO: understand why this skip flag is needed for insertAstMod.
|
||||
// It's unclear why we double casts the AST
|
||||
if (!options?.skipUpdateAst) {
|
||||
// Step 1: Update AST without executing (prepare selections)
|
||||
const updatedAst = await dependencies.kclManager.updateAst(
|
||||
updatedAst = await dependencies.kclManager.updateAst(
|
||||
ast,
|
||||
// false == mock execution. Is this what we want?
|
||||
false, // Execution handled separately for error resilience
|
||||
options
|
||||
)
|
||||
}
|
||||
|
||||
// Step 2: Update the code editor and save file
|
||||
await dependencies.codeManager.updateEditorWithAstAndWriteToFile(
|
||||
|
@ -9,7 +9,10 @@ import {
|
||||
createArrayExpression,
|
||||
createCallExpressionStdLib,
|
||||
createCallExpressionStdLibKw,
|
||||
createExpressionStatement,
|
||||
createIdentifier,
|
||||
createImportAsSelector,
|
||||
createImportStatement,
|
||||
createLabeledArg,
|
||||
createLiteral,
|
||||
createLocalName,
|
||||
@ -778,6 +781,57 @@ export function addOffsetPlane({
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an import call to load a part
|
||||
*/
|
||||
export function addImportAndInsert({
|
||||
node,
|
||||
path,
|
||||
localName,
|
||||
}: {
|
||||
node: Node<Program>
|
||||
path: string
|
||||
localName: string
|
||||
}): {
|
||||
modifiedAst: Node<Program>
|
||||
pathToImportNode: PathToNode
|
||||
pathToInsertNode: PathToNode
|
||||
} {
|
||||
const modifiedAst = structuredClone(node)
|
||||
|
||||
// Add import statement
|
||||
const importStatement = createImportStatement(
|
||||
createImportAsSelector(localName),
|
||||
{ type: 'Kcl', filename: path }
|
||||
)
|
||||
const lastImportIndex = node.body.findLastIndex(
|
||||
(v) => v.type === 'ImportStatement'
|
||||
)
|
||||
const importIndex = lastImportIndex + 1 // either -1 + 1 = 0 or after the last import
|
||||
modifiedAst.body.splice(importIndex, 0, importStatement)
|
||||
const pathToImportNode: PathToNode = [
|
||||
['body', ''],
|
||||
[importIndex, 'index'],
|
||||
['path', 'ImportStatement'],
|
||||
]
|
||||
|
||||
// Add insert statement
|
||||
const insertStatement = createExpressionStatement(createLocalName(localName))
|
||||
const insertIndex = modifiedAst.body.length
|
||||
modifiedAst.body.push(insertStatement)
|
||||
const pathToInsertNode: PathToNode = [
|
||||
['body', ''],
|
||||
[insertIndex, 'index'],
|
||||
['expression', 'ExpressionStatement'],
|
||||
]
|
||||
|
||||
return {
|
||||
modifiedAst,
|
||||
pathToImportNode,
|
||||
pathToInsertNode,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Append a helix to the AST
|
||||
*/
|
||||
|
@ -2,6 +2,9 @@ import type { UnitLength_type } from '@kittycad/lib/dist/types/src/models'
|
||||
import toast from 'react-hot-toast'
|
||||
|
||||
import { CommandBarOverwriteWarning } from '@src/components/CommandBarOverwriteWarning'
|
||||
import { DEV } from '@src/env'
|
||||
import { updateModelingState } from '@src/lang/modelingWorkflows'
|
||||
import { addImportAndInsert } from '@src/lang/modifyAst'
|
||||
import {
|
||||
changeKclSettings,
|
||||
unitAngleToUnitAng,
|
||||
@ -11,14 +14,16 @@ import type { Command, CommandArgumentOption } from '@src/lib/commandTypes'
|
||||
import {
|
||||
DEFAULT_DEFAULT_ANGLE_UNIT,
|
||||
DEFAULT_DEFAULT_LENGTH_UNIT,
|
||||
EXECUTION_TYPE_REAL,
|
||||
FILE_EXT,
|
||||
} from '@src/lib/constants'
|
||||
import { isDesktop } from '@src/lib/isDesktop'
|
||||
import { copyFileShareLink } from '@src/lib/links'
|
||||
import { baseUnitsUnion } from '@src/lib/settings/settingsTypes'
|
||||
import { codeManager, kclManager } from '@src/lib/singletons'
|
||||
import { codeManager, editorManager, kclManager } from '@src/lib/singletons'
|
||||
import { err, reportRejection } from '@src/lib/trap'
|
||||
import type { IndexLoaderData } from '@src/lib/types'
|
||||
import { IS_NIGHTLY_OR_DEBUG } from '@src/routes/utils'
|
||||
|
||||
interface OnSubmitProps {
|
||||
sampleName: string
|
||||
@ -34,6 +39,9 @@ interface KclCommandConfig {
|
||||
onSubmit: (p: OnSubmitProps) => Promise<void>
|
||||
providedOptions: CommandArgumentOption<string>[]
|
||||
}
|
||||
specialPropsForInsertCommand: {
|
||||
providedOptions: CommandArgumentOption<string>[]
|
||||
}
|
||||
projectData: IndexLoaderData
|
||||
authToken: string
|
||||
settings: {
|
||||
@ -96,6 +104,50 @@ export function kclCommands(commandProps: KclCommandConfig): Command[] {
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Insert',
|
||||
description: 'Insert from a file in the current project directory',
|
||||
icon: 'import',
|
||||
groupId: 'code',
|
||||
hide: DEV || IS_NIGHTLY_OR_DEBUG ? 'web' : 'both',
|
||||
needsReview: true,
|
||||
reviewMessage:
|
||||
'Reminder: point-and-click insert is in development and only supports one part instance per assembly.',
|
||||
args: {
|
||||
path: {
|
||||
inputType: 'options',
|
||||
required: true,
|
||||
options: commandProps.specialPropsForInsertCommand.providedOptions,
|
||||
},
|
||||
localName: {
|
||||
inputType: 'string',
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
onSubmit: (data) => {
|
||||
if (!data) {
|
||||
return new Error('No input provided')
|
||||
}
|
||||
|
||||
const ast = kclManager.ast
|
||||
const { path, localName } = data
|
||||
const { modifiedAst, pathToImportNode, pathToInsertNode } =
|
||||
addImportAndInsert({
|
||||
node: ast,
|
||||
path,
|
||||
localName,
|
||||
})
|
||||
updateModelingState(
|
||||
modifiedAst,
|
||||
EXECUTION_TYPE_REAL,
|
||||
{ kclManager, editorManager, codeManager },
|
||||
{
|
||||
skipUpdateAst: true,
|
||||
focusPath: [pathToImportNode, pathToInsertNode],
|
||||
}
|
||||
).catch(reportRejection)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'format-code',
|
||||
displayName: 'Format Code',
|
||||
|
@ -25,6 +25,7 @@ export type MenuLabels =
|
||||
| 'File.Create new file'
|
||||
| 'File.Create new folder'
|
||||
| 'File.Load a sample model'
|
||||
| 'File.Insert from project file'
|
||||
| 'File.Export current part'
|
||||
| 'File.Share current part (via Zoo link)'
|
||||
| 'File.Preferences.Project settings'
|
||||
|
@ -157,6 +157,15 @@ export const modelingFileRole = (
|
||||
},
|
||||
},
|
||||
{ type: 'separator' },
|
||||
{
|
||||
label: 'Insert from project file',
|
||||
id: 'File.Insert from project file',
|
||||
click: () => {
|
||||
typeSafeWebContentsSend(mainWindow, 'menu-action-clicked', {
|
||||
menuLabel: 'File.Insert from project file',
|
||||
})
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Export current part',
|
||||
id: 'File.Export current part',
|
||||
|
@ -92,6 +92,14 @@ export function modelingMenuCallbackMostActions(
|
||||
}).catch(reportRejection)
|
||||
} else if (data.menuLabel === 'File.Preferences.User default units') {
|
||||
navigate(filePath + PATHS.SETTINGS_USER + '#defaultUnit')
|
||||
} else if (data.menuLabel === 'File.Insert from project file') {
|
||||
commandBarActor.send({
|
||||
type: 'Find and select command',
|
||||
data: {
|
||||
groupId: 'code',
|
||||
name: 'Insert',
|
||||
},
|
||||
})
|
||||
} else if (data.menuLabel === 'File.Export current part') {
|
||||
commandBarActor.send({
|
||||
type: 'Find and select command',
|
||||
|
@ -21,6 +21,7 @@ type FileRoleLabel =
|
||||
| 'Sign out'
|
||||
| 'Theme'
|
||||
| 'Theme color'
|
||||
| 'Insert from project file'
|
||||
| 'Export current part'
|
||||
| 'Create new file'
|
||||
| 'Create new folder'
|
||||
|
Reference in New Issue
Block a user