[Fix]: Allow importing assemblies into exsiting projects and handling the collision (#7108)

* fix: saving off code

* fix: saving off progress

* chore: implemented kcl sample assembly unique sub dir creation

* fix: removing testing console logs

* fix: cleaning up old comment

* fix: add to file always does subdir/main.kcl now for single files

* fix: auto fmt

* fix: delete project and folder from ttc

* fix: fixed deleting projects and subdirs

* fix: if statement logic fixed for deleting project or subdir

* fix: TTC isProjectNew makes main.kcl not a subdir.

* fix: fixing e2e test

* fix: this should pass now
This commit is contained in:
Kevin Nadro
2025-05-20 18:03:54 -05:00
committed by GitHub
parent 815ff7dc2b
commit 0753987b5a
6 changed files with 141 additions and 77 deletions

View File

@ -103,6 +103,8 @@ test.describe('Testing loading external models', () => {
file: 'ball-bearing' + FILE_EXT, file: 'ball-bearing' + FILE_EXT,
title: 'Ball Bearing', title: 'Ball Bearing',
file1: 'ball-bearing-1' + FILE_EXT, file1: 'ball-bearing-1' + FILE_EXT,
folderName: 'ball-bearing',
folderName1: 'ball-bearing-1',
} }
const projectCard = page.getByRole('link', { name: 'bracket' }) const projectCard = page.getByRole('link', { name: 'bracket' })
const overwriteWarning = page.getByText( const overwriteWarning = page.getByText(
@ -154,8 +156,10 @@ test.describe('Testing loading external models', () => {
await test.step(`Ensure we made and opened a new file`, async () => { await test.step(`Ensure we made and opened a new file`, async () => {
await editor.expectEditor.toContain('// ' + sampleOne.title) await editor.expectEditor.toContain('// ' + sampleOne.title)
await expect(newlyCreatedFile(sampleOne.file)).toBeVisible() await expect(
await expect(projectMenuButton).toContainText(sampleOne.file) page.getByTestId('file-tree-item').getByText(sampleOne.folderName)
).toBeVisible()
await expect(projectMenuButton).toContainText('main.kcl')
}) })
await test.step(`Load a KCL sample with the command palette`, async () => { await test.step(`Load a KCL sample with the command palette`, async () => {
@ -169,8 +173,10 @@ test.describe('Testing loading external models', () => {
await test.step(`Ensure we made and opened a new file with a unique name`, async () => { await test.step(`Ensure we made and opened a new file with a unique name`, async () => {
await editor.expectEditor.toContain('// ' + sampleOne.title) await editor.expectEditor.toContain('// ' + sampleOne.title)
await expect(newlyCreatedFile(sampleOne.file1)).toBeVisible() await expect(
await expect(projectMenuButton).toContainText(sampleOne.file1) page.getByTestId('file-tree-item').getByText(sampleOne.folderName1)
).toBeVisible()
await expect(projectMenuButton).toContainText('main.kcl')
}) })
} }
) )

View File

@ -984,12 +984,12 @@ test.describe('Mocked Text-to-CAD API tests', { tag: ['@skipWin'] }, () => {
) )
await expect(page.getByTestId('app-header-file-name')).toBeVisible() await expect(page.getByTestId('app-header-file-name')).toBeVisible()
await expect(page.getByTestId('app-header-file-name')).toContainText( await expect(page.getByTestId('app-header-file-name')).toContainText(
'2x2x2-cube.kcl' 'main.kcl'
) )
await u.openFilePanel() await u.openFilePanel()
await expect( await expect(
page.getByTestId('file-tree-item').getByText('2x2x2-cube.kcl') page.getByTestId('file-tree-item').getByText('2x2x2-cube')
).toBeVisible() ).toBeVisible()
} }
) )
@ -1184,13 +1184,13 @@ test.describe('Mocked Text-to-CAD API tests', { tag: ['@skipWin'] }, () => {
) )
await expect(page.getByTestId('app-header-file-name')).toBeVisible() await expect(page.getByTestId('app-header-file-name')).toBeVisible()
await expect(page.getByTestId('app-header-file-name')).toContainText( await expect(page.getByTestId('app-header-file-name')).toContainText(
'2x2x2-cube.kcl' 'main.kcl'
) )
// Check file is created // Check file is created
await u.openFilePanel() await u.openFilePanel()
await expect( await expect(
page.getByTestId('file-tree-item').getByText('2x2x2-cube.kcl') page.getByTestId('file-tree-item').getByText('2x2x2-cube')
).toBeVisible() ).toBeVisible()
} }
) )
@ -1476,13 +1476,13 @@ test.describe('Mocked Text-to-CAD API tests', { tag: ['@skipWin'] }, () => {
) )
await expect(page.getByTestId('app-header-file-name')).toBeVisible() await expect(page.getByTestId('app-header-file-name')).toBeVisible()
await expect(page.getByTestId('app-header-file-name')).toContainText( await expect(page.getByTestId('app-header-file-name')).toContainText(
'2x2x2-cube.kcl' 'main.kcl'
) )
// Check file is created // Check file is created
await u.openFilePanel() await u.openFilePanel()
await expect( await expect(
page.getByTestId('file-tree-item').getByText('2x2x2-cube.kcl') page.getByTestId('file-tree-item').getByText('2x2x2-cube')
).toBeVisible() ).toBeVisible()
await expect( await expect(
page.getByTestId('file-tree-item').getByText('main.kcl') page.getByTestId('file-tree-item').getByText('main.kcl')

View File

@ -159,6 +159,7 @@ export function ToastTextToCadSuccess({
projectName, projectName,
fileName, fileName,
isProjectNew, isProjectNew,
rootProjectName,
}: { }: {
toastId: string toastId: string
data: TextToCad_type & { fileName: string } data: TextToCad_type & { fileName: string }
@ -170,6 +171,7 @@ export function ToastTextToCadSuccess({
projectName: string projectName: string
fileName: string fileName: string
isProjectNew: boolean isProjectNew: boolean
rootProjectName: string
}) { }) {
const wrapperRef = useRef<HTMLDivElement | null>(null) const wrapperRef = useRef<HTMLDivElement | null>(null)
const canvasRef = useRef<HTMLCanvasElement | null>(null) const canvasRef = useRef<HTMLCanvasElement | null>(null)
@ -361,26 +363,23 @@ export function ToastTextToCadSuccess({
if (isDesktop()) { if (isDesktop()) {
// Delete the file from the project // Delete the file from the project
if (projectName && fileName) { if (isProjectNew) {
// You are in the new workflow for text to cad at the global application level // Delete the entire project if it was newly created from text to CAD
if (isProjectNew) { systemIOActor.send({
// Delete the entire project if it was newly created from text to CAD type: SystemIOMachineEvents.deleteProject,
systemIOActor.send({ data: {
type: SystemIOMachineEvents.deleteProject, requestedProjectName: rootProjectName,
data: { },
requestedProjectName: projectName, })
}, } else if (projectName && fileName) {
}) // deletes the folder when inside the modeling page
} else { // The TTC Create will make a subdir, delete that dir with the main.kcl as well
// Only delete the file if the project was preexisting systemIOActor.send({
systemIOActor.send({ type: SystemIOMachineEvents.deleteProject,
type: SystemIOMachineEvents.deleteKCLFile, data: {
data: { requestedProjectName: projectName,
requestedProjectName: projectName, },
requestedFileName: fileName, })
},
})
}
} }
} }

View File

@ -10,12 +10,16 @@ import {
kclSamplesManifestWithNoMultipleFiles, kclSamplesManifestWithNoMultipleFiles,
} from '@src/lib/kclSamples' } from '@src/lib/kclSamples'
import { getUniqueProjectName } from '@src/lib/desktopFS' import { getUniqueProjectName } from '@src/lib/desktopFS'
import { IS_ML_EXPERIMENTAL, PROJECT_ENTRYPOINT } from '@src/lib/constants' import { IS_ML_EXPERIMENTAL } from '@src/lib/constants'
import toast from 'react-hot-toast' import toast from 'react-hot-toast'
import { reportRejection } from '@src/lib/trap' import { reportRejection } from '@src/lib/trap'
import { relevantFileExtensions } from '@src/lang/wasmUtils' import { relevantFileExtensions } from '@src/lang/wasmUtils'
import { getStringAfterLastSeparator, webSafePathSplit } from '@src/lib/paths' import {
import { FILE_EXT } from '@src/lib/constants' getStringAfterLastSeparator,
joinOSPaths,
webSafePathSplit,
} from '@src/lib/paths'
import { getAllSubDirectoriesAtProjectRoot } from '@src/machines/systemIO/snapshotContext'
function onSubmitKCLSampleCreation({ function onSubmitKCLSampleCreation({
sample, sample,
@ -69,20 +73,32 @@ function onSubmitKCLSampleCreation({
}) })
} }
/**
* When adding assemblies to an existing project create the assembly into a unique sub directory
*/
if (!isProjectNew) {
requestedFiles.forEach((requestedFile) => {
const subDirectoryName = projectPathPart
const firstLevelDirectories = getAllSubDirectoriesAtProjectRoot({
projectFolderName: requestedFile.requestedProjectName,
})
const uniqueSubDirectoryName = getUniqueProjectName(
subDirectoryName,
firstLevelDirectories
)
requestedFile.requestedProjectName = joinOSPaths(
requestedFile.requestedProjectName,
uniqueSubDirectoryName
)
})
}
if (requestedFiles.length === 1) { if (requestedFiles.length === 1) {
/**
* Navigates to the single file that could be renamed on disk for duplicates
*/
const folderNameBecomesKCLFileName = projectPathPart + FILE_EXT
// If the project is new create the single file as main.kcl
const requestedFileNameWithExtension = isProjectNew
? PROJECT_ENTRYPOINT
: folderNameBecomesKCLFileName
systemIOActor.send({ systemIOActor.send({
type: SystemIOMachineEvents.importFileFromURL, type: SystemIOMachineEvents.importFileFromURL,
data: { data: {
requestedProjectName: requestedFiles[0].requestedProjectName, requestedProjectName: requestedFiles[0].requestedProjectName,
requestedFileNameWithExtension: requestedFileNameWithExtension, requestedFileNameWithExtension: requestedFiles[0].requestedFileName,
requestedCode: requestedFiles[0].requestedCode, requestedCode: requestedFiles[0].requestedCode,
}, },
}) })
@ -278,10 +294,9 @@ export function createApplicationCommands({
return value return value
}, },
options: ({ argumentsToSubmit }) => { options: ({ argumentsToSubmit }) => {
const samples = const samples = isDesktop()
isDesktop() && argumentsToSubmit.method !== 'existingProject' ? everyKclSample
? everyKclSample : kclSamplesManifestWithNoMultipleFiles
: kclSamplesManifestWithNoMultipleFiles
return samples.map((sample) => { return samples.map((sample) => {
return { return {
value: sample.pathFromProjectDirectoryToFirstFile, value: sample.pathFromProjectDirectoryToFirstFile,
@ -296,17 +311,10 @@ export function createApplicationCommands({
skip: true, skip: true,
options: ({ argumentsToSubmit }, _) => { options: ({ argumentsToSubmit }, _) => {
if (isDesktop() && typeof argumentsToSubmit.sample === 'string') { if (isDesktop() && typeof argumentsToSubmit.sample === 'string') {
const kclSample = findKclSample(argumentsToSubmit.sample) return [
if (kclSample && kclSample.files.length > 1) { { name: 'New project', value: 'newProject', isCurrent: true },
return [ { name: 'Existing project', value: 'existingProject' },
{ name: 'New project', value: 'newProject', isCurrent: true }, ]
]
} else {
return [
{ name: 'New project', value: 'newProject', isCurrent: true },
{ name: 'Existing project', value: 'existingProject' },
]
}
} else { } else {
return [{ name: 'Overwrite', value: 'existingProject' }] return [{ name: 'Overwrite', value: 'existingProject' }]
} }

View File

@ -6,9 +6,9 @@ import {
ToastTextToCadError, ToastTextToCadError,
ToastTextToCadSuccess, ToastTextToCadSuccess,
} from '@src/components/ToastTextToCad' } from '@src/components/ToastTextToCad'
import { FILE_EXT, PROJECT_ENTRYPOINT } from '@src/lib/constants' import { PROJECT_ENTRYPOINT } from '@src/lib/constants'
import crossPlatformFetch from '@src/lib/crossPlatformFetch' import crossPlatformFetch from '@src/lib/crossPlatformFetch'
import { getNextFileName } from '@src/lib/desktopFS' import { getUniqueProjectName } from '@src/lib/desktopFS'
import { isDesktop } from '@src/lib/isDesktop' import { isDesktop } from '@src/lib/isDesktop'
import { kclManager, systemIOActor } from '@src/lib/singletons' import { kclManager, systemIOActor } from '@src/lib/singletons'
import { import {
@ -17,6 +17,8 @@ import {
} from '@src/machines/systemIO/utils' } from '@src/machines/systemIO/utils'
import { reportRejection } from '@src/lib/trap' import { reportRejection } from '@src/lib/trap'
import { toSync } from '@src/lib/utils' import { toSync } from '@src/lib/utils'
import { getAllSubDirectoriesAtProjectRoot } from '@src/machines/systemIO/snapshotContext'
import { joinOSPaths } from '@src/lib/paths'
export async function submitTextToCadPrompt( export async function submitTextToCadPrompt(
prompt: string, prompt: string,
@ -173,7 +175,8 @@ export async function submitAndAwaitTextToKclSystemIO({
} }
) )
let newFileName = '' let newFileName = PROJECT_ENTRYPOINT
let uniqueProjectName = projectName
const textToCadOutputCreated = await textToCadComplete const textToCadOutputCreated = await textToCadComplete
.catch((e) => { .catch((e) => {
@ -197,30 +200,28 @@ export async function submitAndAwaitTextToKclSystemIO({
const TRUNCATED_PROMPT_LENGTH = 24 const TRUNCATED_PROMPT_LENGTH = 24
// Only add the prompt name if it is a preexisting project // Only add the prompt name if it is a preexisting project
newFileName = `${value.prompt const subDirectoryAsPromptName = `${value.prompt
.slice(0, TRUNCATED_PROMPT_LENGTH) .slice(0, TRUNCATED_PROMPT_LENGTH)
.replace(/\s/gi, '-') .replace(/\s/gi, '-')
.replace(/\W/gi, '-') .replace(/\W/gi, '-')
.toLowerCase()}${FILE_EXT}` .toLowerCase()}`
// If the project is new generate a main.kcl
if (isProjectNew) {
newFileName = PROJECT_ENTRYPOINT
}
if (isDesktop()) { if (isDesktop()) {
// We have to preemptively run our unique file name logic, if (!isProjectNew) {
// so that we can pass the unique file name to the toast, // If the project is new, use a sub dir
// and by extension the file-deletion-on-reject logic. const firstLevelDirectories = getAllSubDirectoriesAtProjectRoot({
newFileName = getNextFileName({ projectFolderName: projectName,
entryName: newFileName, })
baseDir: projectName, const uniqueSubDirectoryName = getUniqueProjectName(
}).name subDirectoryAsPromptName,
firstLevelDirectories
)
uniqueProjectName = joinOSPaths(projectName, uniqueSubDirectoryName)
}
systemIOActor.send({ systemIOActor.send({
type: SystemIOMachineEvents.createKCLFile, type: SystemIOMachineEvents.createKCLFile,
data: { data: {
requestedProjectName: projectName, requestedProjectName: uniqueProjectName,
requestedCode: value.code, requestedCode: value.code,
requestedFileNameWithExtension: newFileName, requestedFileNameWithExtension: newFileName,
}, },
@ -251,11 +252,14 @@ export async function submitAndAwaitTextToKclSystemIO({
toastId, toastId,
data: textToCadOutputCreated, data: textToCadOutputCreated,
token, token,
projectName: projectName, // This can be a subdir within the rootProjectName
projectName: uniqueProjectName,
fileName: newFileName, fileName: newFileName,
navigate, navigate,
isProjectNew, isProjectNew,
settings, settings,
// This is always the root project name, no subdir
rootProjectName: projectName,
}), }),
{ {
id: toastId, id: toastId,

View File

@ -1,4 +1,6 @@
import type { FileEntry } from '@src/lib/project'
import { systemIOActor } from '@src/lib/singletons' import { systemIOActor } from '@src/lib/singletons'
import { isArray } from '@src/lib/utils'
export const folderSnapshot = () => { export const folderSnapshot = () => {
const { folders } = systemIOActor.getSnapshot().context const { folders } = systemIOActor.getSnapshot().context
@ -9,3 +11,48 @@ export const defaultProjectFolderNameSnapshot = () => {
const { defaultProjectFolderName } = systemIOActor.getSnapshot().context const { defaultProjectFolderName } = systemIOActor.getSnapshot().context
return defaultProjectFolderName return defaultProjectFolderName
} }
/**
* From the application project directory go down to a project folder and list all the folders at that directory level
* application project directory: /home/documents/zoo-modeling-app-projects/
*
* /home/documents/zoo-modeling-app-projects/car-door/
* ├── handle
* ├── main.kcl
* └── window
*
* The two folders are handle and window
*
* @param {Object} params
* @param {string} params.projectFolderName - The name with no path information.
* @returns {FileEntry[]} An array of subdirectory names found at the root level of the specified project folder.
*/
export const getAllSubDirectoriesAtProjectRoot = ({
projectFolderName,
}: { projectFolderName: string }): FileEntry[] => {
const subDirectories: FileEntry[] = []
const { folders } = systemIOActor.getSnapshot().context
const projectFolder = folders.find((folder) => {
return folder.name === projectFolderName
})
// Find the subdirectories
if (projectFolder) {
// 1st level
const children = projectFolder.children
if (children) {
children.forEach((childFileOrDirectory) => {
// 2nd level
const secondLevelChild = childFileOrDirectory.children
// if secondLevelChild is null then it is a file
if (secondLevelChild && isArray(secondLevelChild)) {
// this is a directory!
subDirectories.push(childFileOrDirectory)
}
})
}
}
return subDirectories
}