Files
modeling-app/src/machines/fileMachine.ts
Kevin Nadro 5a4f8bd522 [Feature]: Enable Text-to-CAD at the application level (#6501)
* chore: saving off skeleton

* fix: saving skeleton

* chore: skeleton for loading projects from project directory path

* chore: cleaning up useless state transition to be an on event direct to action state

* fix: new structure for web vs desktop vs react machine provider code

* chore: saving off skeleton

* fix: skeleton logic for react? going to move it from a string to obj.string

* fix: trying to prevent error element unmount on global react components. This is bricking JS state

* fix: we are so back

* chore: implemented navigating to specfic KCL file

* chore: implementing renaming project

* chore: deleting project

* fix: auto fixes

* fix: old debug/testing file oops

* chore: generic create new file

* chore: skeleton for web create file provide

* chore: basic machine vitest... need to figure out how to get window.electron implemented in vitest?

* chore: save off progress before deleting other project implementation, a few missing features still

* chore: trying a different init skeleton? most likely will migrate

* chore: first attempt of purging projects context provider

* chore: enabling toast for some machine state

* chore: enabling more toast success and error

* chore: writing read write state to the system io based on the project path

* fix: tsc fixes

* fix: use file system watcher, navigate to project after creation via the requestProjectName

* chore: open project command, hooks vs snapshot context helpers

* chore: implemented open and create project for e2e testing. They are hard coded in poor spot for now.

* fix: codespell fixes

* chore: implementing more project commands

* chore: PR improvements for root.tsx

* chore: leaving comment about new Router.tsx layout

* fix: removing debugging code

* fix: rewriting component for readability

* fix: improving web initialization

* chore: implementing import file from url which is not actually that?

* fix: clearing search params on import file from url

* fix: fixed two e2e tests, forgot needsReview when making new command

* fix: fixing some import from url business logic to pass e2e tests

* chore: script for diffing circular deps +/-

* fix: formatting

* fix: massive fix for circular depsga!

* fix: trying to fix some errors and auto fmt

* fix: updating deps

* fix: removing debugging code

* fix: big clean up

* fix: more deletion

* fix: tsc cleanup

* fix: TSC TSC TSC TSC!

* fix: typo fix

* fix: clear query params on web only, desktop not required

* fix: removing unused code

* fmt

* Bring back `trap` removed in merge

* Use explicit types instead of `any`s on arg configs

* Add project commands directly to command palette

* fix: deleting debugging code, from PR review

* fix: this got added back(?)

* fix: using referred type

* fix: more PR clean up

* fix: big block comment for xstate architecture decision

* fix: more pr comment fixes

* fix: saving off logic, need a big cleanup because I hacked it together to get a POC

* fix: extra business?

* fix: merge conflict just added them back why dude

* fix: more PR comments

* fix: big ciruclar deps fix, commandBarActor in appActor

* chore: writing e2e test, still need to fix 3 bugs

* chore: adding more scenarios

* fix: formatting

* fix: fixing tsc errors

* chore: deleting the old text to cad and using the new application level one, almost there

* fix: prompt to edit works

* fix: large push to get 1 text to cad command... the usage is a little buggy with delete and navigate within /file

* fix: settings for highlight edges now works

* chore: adding another e2e test

* fix: cleaning up e2e tests and writing more of them

* fix: tsc type

* chore: more e2e improvements, unique project name  in text to cad

* chore: e2e tests should be good to go

* fix: gotcha comment

* fix: enabled web t2c, codespell fixes

* fix: fixing merge conflcits??

* fix: t2c is back

* Remove spaces in command bar test

* fmt

---------

Co-authored-by: Frank Noirot <frankjohnson1993@gmail.com>
Co-authored-by: lee-at-zoo-corp <lee@zoo.dev>
2025-04-25 19:04:47 -04:00

449 lines
14 KiB
TypeScript

import { assign, fromPromise, setup } from 'xstate'
import type { FileEntry, Project } from '@src/lib/project'
type FileMachineContext = {
project: Project
selectedDirectory: FileEntry
itemsBeingRenamed: (FileEntry | string)[]
}
type FileMachineEvents =
| { type: 'Open file'; data: { name: string } }
| { type: 'Open file in new window'; data: { name: string } }
| {
type: 'Rename file'
data: { oldName: string; newName: string; isDir: boolean }
}
| {
type: 'Create file'
data: {
name: string
makeDir: boolean
content?: string
silent?: boolean
shouldSetToRename?: boolean
targetPathToClone?: string
}
}
| { type: 'Delete file'; data: FileEntry }
| { type: 'Set selected directory'; directory: FileEntry }
| { type: 'navigate'; data: { name: string } }
| {
type: 'xstate.done.actor.read-files'
output: Project
}
| {
type: 'xstate.done.actor.rename-file'
output: {
message: string
oldPath: string
newPath: string
}
}
| {
type: 'xstate.done.actor.create-and-open-file'
output: {
message: string
path: string
shouldSetToRename: boolean
}
}
| {
type: 'xstate.done.actor.create-file'
output: {
path: string
}
}
| {
type: 'xstate.done.actor.delete-file'
output: {
message: string
}
}
| { type: 'assign'; data: { [key: string]: any } }
| { type: 'Refresh' }
| { type: 'Refresh with new project'; data: { project: Project } }
export const fileMachine = setup({
types: {} as {
context: FileMachineContext
events: FileMachineEvents
input: Partial<Pick<FileMachineContext, 'project' | 'selectedDirectory'>>
},
actions: {
setFiles: assign(({ event }) => {
if (event.type !== 'xstate.done.actor.read-files') return {}
return { project: event.output }
}),
setSelectedDirectory: assign(({ event }) => {
if (event.type !== 'Set selected directory') return {}
return { selectedDirectory: event.directory }
}),
addFileToRenamingQueue: assign({
itemsBeingRenamed: ({ context, event }) => {
if (event.type !== 'xstate.done.actor.create-and-open-file')
return context.itemsBeingRenamed
return [...context.itemsBeingRenamed, event.output.path]
},
}),
removeFileFromRenamingQueue: assign({
itemsBeingRenamed: ({ context, event }) => {
if (event.type !== 'xstate.done.actor.rename-file')
return context.itemsBeingRenamed
return context.itemsBeingRenamed.filter(
(path) => path !== event.output.oldPath
)
},
}),
setProject: assign(({ event }) => {
if (event.type !== 'Refresh with new project') return {}
return { project: event.data.project }
}),
navigateToFile: () => {},
openFileInNewWindow: () => {},
renameToastSuccess: () => {},
createToastSuccess: () => {},
toastSuccess: () => {},
toastError: () => {},
},
guards: {
'Name has been changed': ({ event }) => {
if (event.type !== 'xstate.done.actor.rename-file') return false
return event.output.newPath !== event.output.oldPath
},
'Has at least 1 file': ({ event }) => {
if (event.type !== 'xstate.done.actor.read-files') return false
return !!event?.output?.children && event.output.children.length > 0
},
'Is not silent': ({ event }) =>
event.type === 'Create file' ? !event.data.silent : false,
'Should set to rename': ({ event }) =>
(event.type === 'xstate.done.actor.create-and-open-file' &&
event.output.shouldSetToRename) ||
false,
},
actors: {
readFiles: fromPromise(({ input }: { input: Project }) =>
Promise.resolve(input)
),
createAndOpenFile: fromPromise(
(_: {
input: {
name: string
makeDir: boolean
selectedDirectory: FileEntry
targetPathToClone?: string
content?: string
shouldSetToRename: boolean
}
}) => Promise.resolve({ message: '', path: '' })
),
renameFile: fromPromise(
(_: {
input: {
oldName: string
newName: string
isDir: boolean
selectedDirectory: FileEntry
}
}) => Promise.resolve({ message: '', newPath: '', oldPath: '' })
),
deleteFile: fromPromise(
(_: {
input: { path: string; children: FileEntry[] | null; name: string }
}) => Promise.resolve({ message: '' } as { message: string } | undefined)
),
createFile: fromPromise(
(_: {
input: {
name: string
makeDir: boolean
selectedDirectory: FileEntry
content?: string
}
}) => Promise.resolve({ path: '' })
),
},
}).createMachine({
/** @xstate-layout N4IgpgJg5mDOIC5QDECWAbMACAtgQwGMALVAOzAGI9ZZUpSBtABgF1FQAHAe1oBdUupdiAAeiACwAmADQgAnogAcANgCsAOnHiAjOICcAZh3K9TRQHYAvpdlpMuQiXIUASmABmAJzhFmbJCDcfAJCAWIIUrIKCHpq6nraipJJKorahqrWthjY+MRkYOoAEtRYpFxY7jmwFADC3ni82FWYfsJBqPyCwuG6hurKTAYG5mlSyeJRiHqS2prKg6Mj2pKz4lkgdrmOBcWlLXCuYKR4OM05bQEdXaGgvdpD6qPiioqqieJM2gaqUxELT3MQz0eleBlMhmUGy2Dny5D2sEq1TqDSaSNarHaPE6IR6iF0ij08SGklUpikym0qkm8gkJie1KYklB70UBkkTChNk2OVhTkKJURBxq9TAjXOrW0-k42JueIQfQMAyGIzGq0UNOiqg5mi+5nMlM+gxm0N5eX5CPRhwAImBMGiDpcZcFumF8dptMp1GZRpT9YZOXo-ml1IlzMkHuZVOyDGpTfZzbtBVaagB5DjHK1OwKy3FuhX6JWDYajXTqzUSRKh8FMPTa1TmBJDePbOEC-bIgDKYF4WFgdrABCaECwEFQ3iHXE8cmz1zzd3xkmUSveP0UJJWyT+sfE3o94PSbK0KxbfN2osaZCgWDwpBHXAzpCvVooEEEhTIADcuABrQoEVEwAAWlvCAgIfY4gMdTErlzV0FwVH5dx3ZdtUSA9lD+B4qW9EkmE5ZkEnMAxT0TeEL34Uhr1ArAIKfKiXzfeEv1-f9AJAu9wMfKCLilLEXVuURpmUcw91JakDAsLQRiwplFHUIxlDeUxZlGRRSJ2cjUWfGi6OfA4KDATxPCndQOHQRp3CnHB1AAsUmg4sC6J4jFpRzAT5SpHDPRGalRlUJJxF+WkEAbJgFOUZJCQ+NJvg0tt1DcE4cH0nJX3fdQWL-dRvGS4DoLcud4KEhBY1EhJ3iCqkdGGRQ-jJDQmCCwlYyYUYlnii0ktOVLMHS5jSG-bLctOfLeMKuDBPCMr4i8qrqW+SS-nDDRJK0VYXmZcRwU63ZupShiDKMkzPDMizeCszwbJGs4XLAWdJvlEZRMkcQDXDVDY20cxltWdRXjZXQzDUSQdu5GEyMKW17V6ygmI-QbWPUCABwcgr+JxYrpv1dRXvepcfi+n6QoMfDQ2ZALzH3d6rHBs1NKh1HYcM4zTPMyzrOR1GxtcjG5XzZ7cbekSCejP0-g5SR-skprVD9Kkvl2+E3DwMdDuReHMsR4axTA4UHo8-MZnCn5iNjOXXjrbRlpWb12U9Hzo1mdS6YTBnEt12Gak1rLCgaPXqgYPjYMNhDjYUhthjUJTCXeP5TC9SlowsS260JJXChVtXr2FFmTrOjmrpy3W7tgA3Mam6YdVNqOLdj62QvXIkY31YWl0+dZXdbC0KOZn3tbY+yefumDnQrzyGyJNQ3teJTYxMAwsNmIkNpeIKpC+TIu7PLT7OZ462fOy6bLs8U7vL-mEKpdd4j0F52WjUw2ob6IPXDXHUPEYspAMKlrG5coKN4ABAhgzPm84SpAUwiFKBuF8L7iZGoEEPwM6WnKCmcBWN8Skynl-CErx9CqGpPHWYAw2TKRmBySSkhUHJmFJgyuiFvqaAmItN45gdB1RCngzQrxEi6CMB9VBvcGK6UfLDBhnlRhSzLBYVYSktoyBCg8UGTwlJ6HDCkB4RDUH7QkSHce+ZIghUWLjYYeFf4qCCqg6GPZ9Fj0viVQkAxPR+RqloIhxNX6glxuGfCkgOGCKTroz26tMDAIcRA8IkU5hIXXhqDRjYvHTFrOoakd98JlQjNoVB6Zjj2PcoYq+0jQxSDkUuJId8lHRHBCuUYTU-T6mpMI7SYSwCSPzN9JIpTkjhgqYorC30pZtSIdqWMbwAr-0sEAA */
id: 'File machine',
initial: 'Reading files',
context: ({ input }) => {
return {
project: input.project ?? ({} as Project), // TODO: Either make this a flexible type or type this property to allow empty object
selectedDirectory: input.selectedDirectory ?? ({} as FileEntry), // TODO: Either make this a flexible type or type this property to allow empty object
itemsBeingRenamed: [],
}
},
on: {
assign: {
actions: assign(({ event }) => ({
...event.data,
})),
target: '.Reading files',
},
Refresh: '.Reading files',
'Refresh with new project': {
actions: ['setProject'],
target: '.Reading files',
},
},
states: {
'Has no files': {
on: {
'Create file': {
target: 'Creating and opening file',
},
},
},
'Has files': {
on: {
'Rename file': {
target: 'Renaming file',
},
'Create file': [
{
target: 'Creating and opening file',
guard: 'Is not silent',
},
'Creating file',
],
'Delete file': {
target: 'Deleting file',
},
'Open file': {
target: 'Opening file',
},
'Open file in new window': {
target: 'Opening file in new window',
},
'Set selected directory': {
target: 'Has files',
actions: ['setSelectedDirectory'],
},
},
},
'Creating and opening file': {
invoke: {
id: 'create-and-open-file',
src: 'createAndOpenFile',
input: ({ event, context }) => {
if (event.type !== 'Create file')
// This is just to make TS happy
return {
name: '',
makeDir: false,
selectedDirectory: context.selectedDirectory,
content: '',
shouldSetToRename: false,
}
return {
name: event.data.name,
makeDir: event.data.makeDir,
selectedDirectory: context.selectedDirectory,
targetPathToClone: event.data.targetPathToClone,
content: event.data.content,
shouldSetToRename: event.data.shouldSetToRename ?? false,
}
},
onDone: [
{
target: 'Reading files',
actions: [
{
type: 'createToastSuccess',
params: ({
event,
}: {
// TODO: rely on type inference
event: Extract<
FileMachineEvents,
{ type: 'xstate.done.actor.create-and-open-file' }
>
}) => {
return { message: event.output.message }
},
},
'addFileToRenamingQueue',
'navigateToFile',
],
guard: 'Should set to rename',
},
{
target: 'Reading files',
actions: [
{
type: 'createToastSuccess',
params: ({
event,
}: {
// TODO: rely on type inference
event: Extract<
FileMachineEvents,
{ type: 'xstate.done.actor.create-and-open-file' }
>
}) => {
return { message: event.output.message }
},
},
'navigateToFile',
],
},
],
onError: [
{
target: 'Reading files',
actions: ['toastError'],
},
],
},
},
'Renaming file': {
invoke: {
id: 'rename-file',
src: 'renameFile',
input: ({ event, context }) => {
if (event.type !== 'Rename file') {
// This is just to make TS happy
return {
oldName: '',
newName: '',
isDir: false,
selectedDirectory: {} as FileEntry,
}
}
return {
oldName: event.data.oldName,
newName: event.data.newName,
isDir: event.data.isDir,
selectedDirectory: context.selectedDirectory,
}
},
onDone: [
{
target: '#File machine.Reading files',
actions: ['renameToastSuccess'],
guard: 'Name has been changed',
},
'Reading files',
],
onError: [
{
target: '#File machine.Reading files',
actions: ['toastError'],
},
],
},
exit: 'removeFileFromRenamingQueue',
},
'Deleting file': {
invoke: {
id: 'delete-file',
src: 'deleteFile',
input: ({ event }) => {
if (event.type !== 'Delete file') {
// This is just to make TS happy
return {
path: '',
children: [],
name: '',
}
}
return {
path: event.data.path,
children: event.data.children,
name: event.data.name,
}
},
onDone: [
{
actions: ['toastSuccess'],
target: '#File machine.Reading files',
},
],
onError: {
actions: ['toastError'],
target: '#File machine.Has files',
},
},
},
'Reading files': {
invoke: {
id: 'read-files',
src: 'readFiles',
input: ({ context }) => context.project,
onDone: [
{
guard: 'Has at least 1 file',
target: 'Has files',
actions: ['setFiles'],
},
{
target: 'Has no files',
actions: ['setFiles'],
},
],
onError: [
{
target: 'Has no files',
actions: ['toastError'],
},
],
},
},
'Opening file': {
entry: ['navigateToFile'],
},
'Opening file in new window': {
entry: ['openFileInNewWindow'],
},
'Creating file': {
invoke: {
src: 'createFile',
id: 'create-file',
input: ({ event, context }) => {
if (event.type !== 'Create file') {
// This is just to make TS happy
return {
name: '',
makeDir: false,
selectedDirectory: {} as FileEntry,
content: '',
}
}
return {
name: event.data.name,
makeDir: event.data.makeDir,
selectedDirectory: context.selectedDirectory,
content: event.data.content,
}
},
onDone: 'Reading files',
onError: 'Reading files',
},
},
},
})