Assemblies: Load outside files into project via point-and-click (#6217)

* 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

* WIP: UX improvements around foreign file imports
Fixes #6152

* @lrev-Dev's suggestion to remove a comment

Co-authored-by: Kurt Hutten <k.hutten@protonmail.ch>

* Update to scene.settled(cmdBar)

* Add partNNN default name for alias

* Lint

* Lint

* Fix unit tests

* Add sad path insert test
Thanks @Irev-Dev for the suggestion

* Add step insert test

* Lint

* Add test for second foreign import thru file tree click

* WIP: Add point-and-click Load to copy files from outside the project into the project
Towards #6210

* Move Insert button to modeling toolbar, update menus and toolbars

* Add default value for local name alias

* Aligning tests

* Fix tests

* Add padding for filenames starting with a digit

* Lint

* Lint

* Update snapshots

* Merge branch 'main' into pierremtb/issue6210-Add-point-and-click-Load-to-copy-files-from-outside-the-project-into-the-project

* Add disabled transform subbutton

* Merge kcl-samples and local disk load into one 'Load external model' command

* Fix em tests

* Fix test

* Add test for file pick import, better input

* Fix non .kcl loading

* Lint

* Update snapshots

* Fix issue leading to test failure

* Fix clone test

* Add note

* Fix nested clone issue

* Clean up for review

* Add valueSummary for path

* Fix test after path change

* Clean up for review

* Update src/lib/kclCommands.ts

Thanks @franknoirot!

Co-authored-by: Frank Noirot <frank@zoo.dev>

* Improve path input arg

* Fix tests

* Merge branch 'main' into pierremtb/issue6210-Add-point-and-click-Load-to-copy-files-from-outside-the-project-into-the-project

* Fix path header not showing and improve tests

* Clean up

---------

Co-authored-by: Kurt Hutten <k.hutten@protonmail.ch>
Co-authored-by: Frank Noirot <frank@zoo.dev>
This commit is contained in:
Pierre Jacquier
2025-04-14 14:53:01 -04:00
committed by GitHub
parent 39af110ac1
commit add1b21503
44 changed files with 552 additions and 186 deletions

View File

@ -2,6 +2,7 @@ import CommandArgOptionInput from '@src/components/CommandBar/CommandArgOptionIn
import CommandBarBasicInput from '@src/components/CommandBar/CommandBarBasicInput'
import CommandBarHeader from '@src/components/CommandBar/CommandBarHeader'
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'
@ -108,6 +109,14 @@ function ArgumentInput({
onSubmit={onSubmit}
/>
)
case 'path':
return (
<CommandBarPathInput
arg={arg}
stepBack={stepBack}
onSubmit={onSubmit}
/>
)
default:
return (
<CommandBarBasicInput

View File

@ -0,0 +1,120 @@
import { useEffect, useMemo, useRef } from 'react'
import { useHotkeys } from 'react-hotkeys-hook'
import { ActionButton } from '@src/components/ActionButton'
import type { CommandArgument } from '@src/lib/commandTypes'
import { reportRejection } from '@src/lib/trap'
import { isArray, toSync } from '@src/lib/utils'
import {
commandBarActor,
useCommandBarState,
} from '@src/machines/commandBarMachine'
import { useSelector } from '@xstate/react'
import type { AnyStateMachine, SnapshotFrom } from 'xstate'
// TODO: remove the need for this selector once we decouple all actors from React
const machineContextSelector = (snapshot?: SnapshotFrom<AnyStateMachine>) =>
snapshot?.context
function CommandBarPathInput({
arg,
stepBack,
onSubmit,
}: {
arg: CommandArgument<unknown> & {
inputType: 'path'
name: string
}
stepBack: () => void
onSubmit: (event: unknown) => void
}) {
const commandBarState = useCommandBarState()
useHotkeys('mod + k, mod + /', () => commandBarActor.send({ type: 'Close' }))
const inputRef = useRef<HTMLInputElement>(null)
const argMachineContext = useSelector(
arg.machineActor,
machineContextSelector
)
const defaultValue = useMemo(
() =>
arg.defaultValue
? arg.defaultValue instanceof Function
? arg.defaultValue(commandBarState.context, argMachineContext)
: arg.defaultValue
: '',
[arg.defaultValue, commandBarState.context, argMachineContext]
)
function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault()
onSubmit(inputRef.current?.value)
}
async function pickFileThroughNativeDialog() {
// In desktop end-to-end tests we can't control the file picker,
// so we seed the new directory value in the element's dataset
const inputRefVal = inputRef.current?.dataset.testValue
if (inputRef.current && inputRefVal && !isArray(inputRefVal)) {
inputRef.current.value = inputRefVal
} else if (inputRef.current) {
const newPath = await window.electron.open({
properties: ['openFile'],
title: 'Pick a file to load into the current project',
})
if (newPath.canceled) return
inputRef.current.value = newPath.filePaths[0]
} else {
return new Error("Couldn't find inputRef")
}
}
// Fire on component mount, if outside of e2e test context
useEffect(() => {
window.electron.process.env.IS_PLAYWRIGHT !== 'true' &&
toSync(pickFileThroughNativeDialog, reportRejection)()
}, [])
return (
<form id="arg-form" onSubmit={handleSubmit}>
<label
data-testid="cmd-bar-arg-name"
className="flex items-center mx-4 my-4 border-b border-b-chalkboard-100 dark:border-b-chalkboard-80"
>
<span className="capitalize px-2 py-1 bg-chalkboard-100 dark:bg-chalkboard-80 text-chalkboard-10">
{arg.displayName || arg.name}
</span>
<input
type="text"
data-testid="cmd-bar-arg-value"
id="arg-form"
name={arg.inputType}
ref={inputRef}
required
className="flex-grow px-2 py-1 !bg-transparent focus:outline-none"
placeholder="Enter a path"
defaultValue={defaultValue}
onKeyDown={(event) => {
if (event.key === 'Backspace' && event.shiftKey) {
stepBack()
}
}}
/>
<ActionButton
Element="button"
onClick={toSync(pickFileThroughNativeDialog, reportRejection)}
className="p-0 m-0 border-none hover:bg-primary/10 focus:bg-primary/10 dark:hover:bg-primary/20 dark:focus::bg-primary/20"
data-testid="cmd-bar-arg-file-button"
iconEnd={{
icon: 'file',
size: 'sm',
className: 'p-1',
}}
>
Open file
</ActionButton>
</label>
</form>
)
}
export default CommandBarPathInput

View File

@ -619,6 +619,22 @@ const CustomIconMap = {
/>
</svg>
),
importFile: (
<svg
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M4 3H4.5H11H11.2071L11.3536 3.14645L15.8536 7.64646L16 7.7929V8.00001V11.3773C15.6992 11.1362 15.3628 10.9376 15 10.7908V8.50001H11H10.5V8.00001V4H5V16H9.79076C9.93763 16.3628 10.1362 16.6992 10.3773 17H4.5H4V16.5V3.5V3ZM11.5 4.70711L14.2929 7.50001H11.5V4.70711ZM13.8123 17.3904L16.3123 15.3904L15.6877 14.6096L14 15.9597V12H13V15.9597L11.3123 14.6096L10.6877 15.3904L13.1877 17.3904L13.5 17.6403L13.8123 17.3904Z"
fill="currentColor"
/>
</svg>
),
'intersection-offset': (
<svg
viewBox="0 0 20 20"

View File

@ -228,16 +228,30 @@ export const FileMachineProvider = ({
createdPath = path
await window.electron.mkdir(createdPath)
} else {
const { name, path } = getNextFileName({
entryName: input.targetPathToClone
? window.electron.path.basename(input.targetPathToClone)
: createdName,
baseDir: input.targetPathToClone
? window.electron.path.dirname(input.targetPathToClone)
: input.selectedDirectory.path,
})
createdName = name
createdPath = path
const isTargetPathToCloneASubPath =
input.targetPathToClone &&
input.selectedDirectory.path.indexOf(input.targetPathToClone) > -1
if (isTargetPathToCloneASubPath) {
const { name, path } = getNextFileName({
entryName: input.targetPathToClone
? window.electron.path.basename(input.targetPathToClone)
: createdName,
baseDir: input.targetPathToClone
? window.electron.path.dirname(input.targetPathToClone)
: input.selectedDirectory.path,
})
createdName = name
createdPath = path
} else {
const { name, path } = getNextFileName({
entryName: input.targetPathToClone
? window.electron.path.basename(input.targetPathToClone)
: createdName,
baseDir: input.selectedDirectory.path,
})
createdName = name
createdPath = path
}
if (input.targetPathToClone) {
await window.electron.copyFile(
input.targetPathToClone,
@ -437,19 +451,19 @@ export const FileMachineProvider = ({
settings.modeling.defaultUnit.current ??
DEFAULT_DEFAULT_LENGTH_UNIT,
},
specialPropsForSampleCommand: {
specialPropsForLoadCommand: {
onSubmit: async (data) => {
if (data.method === 'overwrite') {
codeManager.updateCodeStateEditor(data.code)
if (data.method === 'overwrite' && data.content) {
codeManager.updateCodeStateEditor(data.content)
await kclManager.executeCode()
await codeManager.writeToFile()
} else if (data.method === 'newFile' && isDesktop()) {
send({
type: 'Create file',
data: {
name: data.sampleName,
content: data.code,
...data,
makeDir: false,
shouldSetToRename: false,
},
})
}
@ -480,7 +494,7 @@ export const FileMachineProvider = ({
}),
},
}).filter(
(command) => kclSamples.length || command.name !== 'open-kcl-example'
(command) => kclSamples.length || command.name !== 'load-external-model'
),
[codeManager, kclManager, send, kclSamples, project, file]
)

View File

@ -90,13 +90,13 @@ export const KclEditorMenu = ({ children }: PropsWithChildren) => {
type: 'Find and select command',
data: {
groupId: 'code',
name: 'open-kcl-example',
name: 'load-external-model',
},
})
}}
className={styles.button}
>
<span>Load a sample model</span>
<span>Load external model</span>
</button>
</Menu.Item>
<Menu.Item>

View File

@ -15,7 +15,6 @@ 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 { useNetworkContext } from '@src/hooks/useNetworkContext'
import { NetworkHealthState } from '@src/hooks/useNetworkStatus'
@ -26,7 +25,6 @@ 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'
@ -77,16 +75,15 @@ export function ModelingSidebar({ paneOpacity }: ModelingSidebarProps) {
const sidebarActions: SidebarAction[] = [
{
id: 'insert',
title: 'Insert from project file',
sidebarName: 'Insert from project file',
icon: 'import',
id: 'load-external-model',
title: 'Load external model',
sidebarName: 'Load external model',
icon: 'importFile',
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' },
data: { name: 'load-external-model', groupId: 'code' },
}),
},
{