From dddc7e38735c737bcf611d08f2d6e88d3cfa888c Mon Sep 17 00:00:00 2001 From: Kevin Date: Wed, 18 Jun 2025 13:58:53 -0500 Subject: [PATCH] fix: big add file and folder logic --- src/components/Explorer/FileExplorer.tsx | 4 +- src/components/Explorer/ProjectExplorer.tsx | 119 ++++++++++++------ src/components/Explorer/utils.ts | 24 ++-- src/lib/paths.ts | 7 ++ src/machines/systemIO/systemIOMachine.ts | 96 +++++++++++++- .../systemIO/systemIOMachineDesktop.ts | 60 ++++++++- src/machines/systemIO/utils.ts | 6 + 7 files changed, 265 insertions(+), 51 deletions(-) diff --git a/src/components/Explorer/FileExplorer.tsx b/src/components/Explorer/FileExplorer.tsx index 3a39fb033..1316bc4b7 100644 --- a/src/components/Explorer/FileExplorer.tsx +++ b/src/components/Explorer/FileExplorer.tsx @@ -170,7 +170,7 @@ function RenameForm({ autoCapitalize="off" autoCorrect="off" placeholder={row.name} - className="w-full py-1 bg-transparent text-chalkboard-100 placeholder:text-chalkboard-70 dark:text-chalkboard-10 dark:placeholder:text-chalkboard-50 focus:outline-none focus:ring-0" + className="overflow-hidden whitespace-nowrap text-ellipsis py-1 bg-transparent text-chalkboard-100 placeholder:text-chalkboard-70 dark:text-chalkboard-10 dark:placeholder:text-chalkboard-50 focus:outline-none focus:ring-0" onKeyDown={handleKeyDown} onBlur={onSubmit} /> @@ -247,7 +247,7 @@ export const FileExplorerRowElement = ({ name={row.icon} className="inline-block w-4 text-current mr-1" /> - {!isMyRowRenaming ? ( + {!isMyRowRenaming && !row.isFake ? ( {row.name} diff --git a/src/components/Explorer/ProjectExplorer.tsx b/src/components/Explorer/ProjectExplorer.tsx index 9837a8c05..412c7d47d 100644 --- a/src/components/Explorer/ProjectExplorer.tsx +++ b/src/components/Explorer/ProjectExplorer.tsx @@ -9,10 +9,12 @@ import { CONTAINER_IS_SELECTED, STARTING_INDEX_TO_SELECT, FILE_PLACEHOLDER_NAME, - FOLDER_PLACEHOLDER_NAME + FOLDER_PLACEHOLDER_NAME, +} from '@src/components/Explorer/utils' +import type { + FileExplorerEntry, + FileExplorerRow, } from '@src/components/Explorer/utils' -import type { FileExplorerEntry, - FileExplorerRow } from '@src/components/Explorer/utils' import { FileExplorerHeaderActions } from '@src/components/Explorer/FileExplorerHeaderActions' import { useState, useRef, useEffect } from 'react' import { systemIOActor } from '@src/lib/singletons' @@ -20,7 +22,9 @@ import { SystemIOMachineEvents } from '@src/machines/systemIO/utils' import { sortFilesAndDirectories } from '@src/lib/desktopFS' import { alwaysEndFileWithEXT, + desktopSafePathSplit, getEXTWithPeriod, + getParentAbsolutePath, joinOSPaths, } from '@src/lib/paths' import { useProjectDirectoryPath } from '@src/machines/systemIO/hooks' @@ -191,6 +195,7 @@ export const ProjectExplorer = ({ // TODO: Implement renameFolder and renameFile to navigate setIsRenaming(false) isRenamingRef.current = false + setFakeRow(null) if (!event) { return @@ -205,28 +210,37 @@ export const ProjectExplorer = ({ // Rename a folder if (row.isFolder) { if (requestedName !== name) { - systemIOActor.send({ - type: SystemIOMachineEvents.renameFolder, - data: { - requestedFolderName: requestedName, - folderName: name, - absolutePathToParentDirectory: joinOSPaths( - projectDirectoryPath, - child.parentPath - ), - }, - }) - // TODO: Gotcha... Set new string open even if it fails? - if (openedRowsRef.current[child.key]) { - // If the file tree had the folder opened make the new one open. - const newOpenedRows = { ...openedRowsRef.current } - const key = constructPath({ - parentPath: child.parentPath, - name: requestedName, + if (row.isFake) { + // create + systemIOActor.send({ + type: SystemIOMachineEvents.createBlankFolder, + data: { + requestedAbsolutePath: joinOSPaths(getParentAbsolutePath(row.path), requestedName) + }, }) - newOpenedRows[key] = true - setOpenedRows(newOpenedRows) + } else { + // rename + systemIOActor.send({ + type: SystemIOMachineEvents.renameFolder, + data: { + requestedFolderName: requestedName, + folderName: name, + absolutePathToParentDirectory: getParentAbsolutePath(row.path) + }, + }) + // TODO: Gotcha... Set new string open even if it fails? + if (openedRowsRef.current[child.key]) { + // If the file tree had the folder opened make the new one open. + const newOpenedRows = { ...openedRowsRef.current } + const key = constructPath({ + parentPath: child.parentPath, + name: requestedName, + }) + newOpenedRows[key] = true + setOpenedRows(newOpenedRows) + } } + } } else { // rename a file @@ -239,21 +253,29 @@ export const ProjectExplorer = ({ // TODO: OH NO! return } - systemIOActor.send({ + + // create a file if it is fake + if (row.isFake) { + systemIOActor.send({ + type: SystemIOMachineEvents.createBlankFile, + data: { + requestedAbsolutePath: joinOSPaths(getParentAbsolutePath(row.path),fileNameForcedWithOriginalExt) + }, + }) + } else { + // rename the file otherwise + systemIOActor.send({ type: SystemIOMachineEvents.renameFile, data: { requestedFileNameWithExtension: fileNameForcedWithOriginalExt, fileNameWithExtension: name, - absolutePathToParentDirectory: joinOSPaths( - projectDirectoryPath, - child.parentPath - ), + absolutePathToParentDirectory: getParentAbsolutePath(row.path) }, }) + } } }, } - return row }) || [] @@ -261,16 +283,39 @@ export const ProjectExplorer = ({ let showPlaceHolder = false if (fakeRow?.isFile) { // fake row is a file - const showFileAtSameLevel = fakeRow?.entry?.parentPath === row.parentPath && !row.isFolder === (fakeRow?.entry?.children === null) && row.name === FILE_PLACEHOLDER_NAME - const showFileWithinFolder = !row.isFolder && !!fakeRow?.entry?.children && fakeRow?.entry?.key === row.parentPath - showPlaceHolder = showFileAtSameLevel || showFileWithinFolder - } else { + const showFileAtSameLevel = + fakeRow?.entry?.parentPath === row.parentPath && + !row.isFolder === (fakeRow?.entry?.children === null) && + row.name === FILE_PLACEHOLDER_NAME + const showFileWithinFolder = + !row.isFolder && + !!fakeRow?.entry?.children && + fakeRow?.entry?.key === row.parentPath && + row.name === FILE_PLACEHOLDER_NAME + const fakeRowIsNullShowRootFile = fakeRow.entry === null && row.parentPath === project.name && + row.name === FILE_PLACEHOLDER_NAME + showPlaceHolder = showFileAtSameLevel || showFileWithinFolder || fakeRowIsNullShowRootFile + } else if (fakeRow?.isFile === false){ // fake row is a folder - const showFolderAtSameLevel = fakeRow?.entry?.parentPath === row.parentPath && !row.isFolder === (!!fakeRow?.entry?.children) && row.name === FOLDER_PLACEHOLDER_NAME - const showFolderWithinFolder = row.isFolder && !!fakeRow?.entry?.children && fakeRow?.entry?.key === row.parentPath - showPlaceHolder = showFolderAtSameLevel || showFolderWithinFolder + const showFolderAtSameLevel = + fakeRow?.entry?.parentPath === row.parentPath && + !row.isFolder === !!fakeRow?.entry?.children && + row.name === FOLDER_PLACEHOLDER_NAME + const showFolderWithinFolder = + row.isFolder && + !!fakeRow?.entry?.children && + fakeRow?.entry?.key === row.parentPath && + row.name === FOLDER_PLACEHOLDER_NAME + const fakeRowIsNullShowRootFolder = fakeRow.entry === null && row.parentPath === project.name && + row.name === FOLDER_PLACEHOLDER_NAME + showPlaceHolder = showFolderAtSameLevel || showFolderWithinFolder || fakeRowIsNullShowRootFolder } - const skipPlaceHolder = !(row.name === FILE_PLACEHOLDER_NAME || row.name === FOLDER_PLACEHOLDER_NAME) || showPlaceHolder + const skipPlaceHolder = + !( + row.name === FILE_PLACEHOLDER_NAME || + row.name === FOLDER_PLACEHOLDER_NAME + ) || showPlaceHolder + row.isFake = showPlaceHolder return row.render && skipPlaceHolder }) diff --git a/src/components/Explorer/utils.ts b/src/components/Explorer/utils.ts index 47c5620c9..4499596f6 100644 --- a/src/components/Explorer/utils.ts +++ b/src/components/Explorer/utils.ts @@ -130,26 +130,32 @@ export const flattenProject = ( return flattenTreeInOrder } -export const addPlaceHoldersForNewFileAndFolder = (children: FileEntry[] | null, parentPath: string) => { +export const addPlaceHoldersForNewFileAndFolder = ( + children: FileEntry[] | null, + parentPath: string +) => { if (children === null) { return } - for( let i = 0; i < children.length; i++) { - addPlaceHoldersForNewFileAndFolder(children[i].children, joinOSPaths(parentPath, children[i].name)) + for (let i = 0; i < children.length; i++) { + addPlaceHoldersForNewFileAndFolder( + children[i].children, + joinOSPaths(parentPath, children[i].name) + ) } - - const placeHolderFolderEntry : FileEntry = { + + const placeHolderFolderEntry: FileEntry = { path: joinOSPaths(parentPath, FOLDER_PLACEHOLDER_NAME), name: FOLDER_PLACEHOLDER_NAME, - children: [] + children: [], } children.unshift(placeHolderFolderEntry) - const placeHolderFileEntry : FileEntry = { + const placeHolderFileEntry: FileEntry = { path: joinOSPaths(parentPath, FILE_PLACEHOLDER_NAME), name: FILE_PLACEHOLDER_NAME, - children: null + children: null, } children.push(placeHolderFileEntry) } @@ -159,4 +165,4 @@ export const NOTHING_IS_SELECTED: number = -2 export const CONTAINER_IS_SELECTED: number = -1 export const STARTING_INDEX_TO_SELECT: number = 0 export const FOLDER_PLACEHOLDER_NAME = '.zoo-placeholder-folder' -export const FILE_PLACEHOLDER_NAME = '.zoo-placeholder-file' +export const FILE_PLACEHOLDER_NAME = '.zoo-placeholder-file.kcl' diff --git a/src/lib/paths.ts b/src/lib/paths.ts index ad28cad21..9c410fa52 100644 --- a/src/lib/paths.ts +++ b/src/lib/paths.ts @@ -245,3 +245,10 @@ export const getEXTWithPeriod = (filePath: string) => { } return extension } + +export const getParentAbsolutePath = (absolutePath: string) => { + const split = desktopSafePathSplit(absolutePath) + split.pop() + const joined = desktopSafePathJoin(split) + return joined +} \ No newline at end of file diff --git a/src/machines/systemIO/systemIOMachine.ts b/src/machines/systemIO/systemIOMachine.ts index a21c9a294..6be5b13b0 100644 --- a/src/machines/systemIO/systemIOMachine.ts +++ b/src/machines/systemIO/systemIOMachine.ts @@ -162,7 +162,19 @@ export const systemIOMachine = setup({ data: { requestedPath: string } - }, + } + | { + type: SystemIOMachineEvents.createBlankFile + data: { + requestedAbsolutePath: string + } + } + | { + type: SystemIOMachineEvents.createBlankFolder + data: { + requestedAbsolutePath: string + } + } }, actions: { [SystemIOMachineActions.setFolders]: assign({ @@ -442,6 +454,38 @@ export const systemIOMachine = setup({ } } ), + [SystemIOMachineActors.createBlankFile]: fromPromise( + async ({ + input, + }: { + input: { + context: SystemIOContext + rootContext: AppMachineContext + requestedAbsolutePath: string + } + }) => { + return { + message: '', + requestedAbsolutePath: '', + } + } + ), + [SystemIOMachineActors.createBlankFolder]: fromPromise( + async ({ + input, + }: { + input: { + context: SystemIOContext + rootContext: AppMachineContext + requestedAbsolutePath: string + } + }) => { + return { + message: '', + requestedAbsolutePath: '', + } + } + ), }, }).createMachine({ initial: SystemIOMachineStates.idle, @@ -529,6 +573,12 @@ export const systemIOMachine = setup({ [SystemIOMachineEvents.deleteFileOrFolder]: { target: SystemIOMachineStates.deletingFileOrFolder, }, + [SystemIOMachineEvents.createBlankFile]: { + target: SystemIOMachineStates.creatingBlankFile, + }, + [SystemIOMachineEvents.createBlankFolder]: { + target: SystemIOMachineStates.creatingBlankFolder, + }, }, }, [SystemIOMachineStates.readingFolders]: { @@ -919,5 +969,49 @@ export const systemIOMachine = setup({ }, }, }, + [SystemIOMachineStates.creatingBlankFile]: { + invoke: { + id: SystemIOMachineActors.createBlankFile, + src: SystemIOMachineActors.createBlankFile, + input: ({ context, event, self }) => { + assertEvent(event, SystemIOMachineEvents.createBlankFile) + return { + context, + requestedAbsolutePath: event.data.requestedAbsolutePath, + rootContext: self.system.get('root').getSnapshot().context, + } + }, + onDone: { + target: SystemIOMachineStates.readingFolders, + actions: [SystemIOMachineActions.toastSuccess], + }, + onError: { + target: SystemIOMachineStates.idle, + actions: [SystemIOMachineActions.toastError], + }, + }, + }, + [SystemIOMachineStates.creatingBlankFolder]: { + invoke: { + id: SystemIOMachineActors.createBlankFolder, + src: SystemIOMachineActors.createBlankFolder, + input: ({ context, event, self }) => { + assertEvent(event, SystemIOMachineEvents.createBlankFolder) + return { + context, + requestedAbsolutePath: event.data.requestedAbsolutePath, + rootContext: self.system.get('root').getSnapshot().context, + } + }, + onDone: { + target: SystemIOMachineStates.readingFolders, + actions: [SystemIOMachineActions.toastSuccess], + }, + onError: { + target: SystemIOMachineStates.idle, + actions: [SystemIOMachineActions.toastError], + }, + }, + }, }, }) diff --git a/src/machines/systemIO/systemIOMachineDesktop.ts b/src/machines/systemIO/systemIOMachineDesktop.ts index 3c07d7c2b..7c96caa65 100644 --- a/src/machines/systemIO/systemIOMachineDesktop.ts +++ b/src/machines/systemIO/systemIOMachineDesktop.ts @@ -270,12 +270,18 @@ export const systemIOMachineDesktop = systemIOMachine.provide({ const configuration = await readAppSettingsFile() // Create the project around the file if newProject - await createNewProjectDirectory( + try { + const result = await createNewProjectDirectory( newProjectName, requestedCode, configuration, newFileName - ) + ) + console.log(result) + } catch (e) { + console.error(e) + } + return { message: 'File created successfully', @@ -526,5 +532,55 @@ export const systemIOMachineDesktop = systemIOMachine.provide({ } } ), + [SystemIOMachineActors.createBlankFile]: fromPromise( + async ({ + input, + }: { + input: { + context: SystemIOContext + rootContext: AppMachineContext + requestedAbsolutePath: string + } + }) => { + try { + const result = await window.electron.stat(input.requestedAbsolutePath) + if (result) { + return Promise.reject(new Error(`File ${input.requestedAbsolutePath} already exists`)) + } + } catch (e) { + console.error(e) + } + await window.electron.writeFile(input.requestedAbsolutePath, '') + return { + message: `File ${input.requestedAbsolutePath} written successfully`, + requestedAbsolutePath: input.requestedAbsolutePath, + } + } + ), + [SystemIOMachineActors.createBlankFolder]: fromPromise( + async ({ + input, + }: { + input: { + context: SystemIOContext + rootContext: AppMachineContext + requestedAbsolutePath: string + } + }) => { + try { + const result = await window.electron.stat(input.requestedAbsolutePath) + if (result) { + return Promise.reject(new Error(`Folder ${input.requestedAbsolutePath} already exists`)) + } + } catch (e) { + console.error(e) + } + await window.electron.mkdir(input.requestedAbsolutePath, {recursive: true}) + return { + message: `File ${input.requestedAbsolutePath} written successfully`, + requestedAbsolutePath: input.requestedAbsolutePath, + } + } + ), }, }) diff --git a/src/machines/systemIO/utils.ts b/src/machines/systemIO/utils.ts index f69cd7a31..5e00f486e 100644 --- a/src/machines/systemIO/utils.ts +++ b/src/machines/systemIO/utils.ts @@ -19,6 +19,8 @@ export enum SystemIOMachineActors { renameFolder = 'renameFolder', renameFile = 'renameFile', deleteFileOrFolder = 'deleteFileOrFolder', + createBlankFile = 'create blank file', + createBlankFolder = 'create blank folder' } export enum SystemIOMachineStates { @@ -39,6 +41,8 @@ export enum SystemIOMachineStates { renamingFolder = 'renamingFolder', renamingFile = 'renamingFile', deletingFileOrFolder = 'deletingFileOrFolder', + creatingBlankFile = 'creatingBlankFile', + creatingBlankFolder = 'creatingBlankFolder' } const donePrefix = 'xstate.done.actor.' @@ -70,6 +74,8 @@ export enum SystemIOMachineEvents { renameFolder = 'rename folder', renameFile = 'rename file', deleteFileOrFolder = 'delete file or folder', + createBlankFile = 'create blank file', + createBlankFolder = 'create blank folder' } export enum SystemIOMachineActions {