Files
modeling-app/src/components/FileMachineProvider.tsx
2024-04-25 12:52:08 +00:00

147 lines
4.3 KiB
TypeScript

import { useMachine } from '@xstate/react'
import { useNavigate, useRouteLoaderData } from 'react-router-dom'
import { type IndexLoaderData } from 'lib/types'
import { paths } from 'lib/paths'
import React, { createContext } from 'react'
import { toast } from 'react-hot-toast'
import {
AnyStateMachine,
ContextFrom,
EventFrom,
InterpreterFrom,
Prop,
StateFrom,
} from 'xstate'
import { useCommandsContext } from 'hooks/useCommandsContext'
import { fileMachine } from 'machines/fileMachine'
import { mkdir, remove, rename, create } from '@tauri-apps/plugin-fs'
import { isTauri } from 'lib/isTauri'
import { join, sep } from '@tauri-apps/api/path'
import { DEFAULT_FILE_NAME, FILE_EXT } from 'lib/constants'
import { getProjectInfo } from 'lib/tauri'
type MachineContext<T extends AnyStateMachine> = {
state: StateFrom<T>
context: ContextFrom<T>
send: Prop<InterpreterFrom<T>, 'send'>
}
export const FileContext = createContext(
{} as MachineContext<typeof fileMachine>
)
export const FileMachineProvider = ({
children,
}: {
children: React.ReactNode
}) => {
const navigate = useNavigate()
const { commandBarSend } = useCommandsContext()
const { project } = useRouteLoaderData(paths.FILE) as IndexLoaderData
const [state, send] = useMachine(fileMachine, {
context: {
project,
selectedDirectory: project,
},
actions: {
navigateToFile: (context, event) => {
if (event.data && 'name' in event.data) {
commandBarSend({ type: 'Close' })
navigate(
`${paths.FILE}/${encodeURIComponent(
context.selectedDirectory + sep() + event.data.name
)}`
)
}
},
toastSuccess: (_, event) =>
event.data && toast.success((event.data || '') + ''),
toastError: (_, event) => toast.error((event.data || '') + ''),
},
services: {
readFiles: async (context: ContextFrom<typeof fileMachine>) => {
const newFiles = isTauri()
? (await getProjectInfo(context.project.path)).children
: []
return {
...context.project,
children: newFiles,
}
},
createFile: async (context, event) => {
let name = event.data.name.trim() || DEFAULT_FILE_NAME
if (event.data.makeDir) {
await mkdir(await join(context.selectedDirectory.path, name))
} else {
await create(
context.selectedDirectory.path +
sep() +
name +
(name.endsWith(FILE_EXT) ? '' : FILE_EXT)
)
}
return `Successfully created "${name}"`
},
renameFile: async (
context: ContextFrom<typeof fileMachine>,
event: EventFrom<typeof fileMachine, 'Rename file'>
) => {
const { oldName, newName, isDir } = event.data
let name = newName ? newName : DEFAULT_FILE_NAME
await rename(
await join(context.selectedDirectory.path, oldName),
(await join(context.selectedDirectory.path, name)) +
(name.endsWith(FILE_EXT) || isDir ? '' : FILE_EXT),
{}
)
return (
oldName !== name && `Successfully renamed "${oldName}" to "${name}"`
)
},
deleteFile: async (
context: ContextFrom<typeof fileMachine>,
event: EventFrom<typeof fileMachine, 'Delete file'>
) => {
const isDir = !!event.data.children
if (isDir) {
await remove(event.data.path, {
recursive: true,
}).catch((e) => console.error('Error deleting directory', e))
} else {
await remove(event.data.path).catch((e) =>
console.error('Error deleting file', e)
)
}
return `Successfully deleted ${isDir ? 'folder' : 'file'} "${
event.data.name
}"`
},
},
guards: {
'Has at least 1 file': (_, event: EventFrom<typeof fileMachine>) => {
if (event.type !== 'done.invoke.read-files') return false
return !!event?.data?.children && event.data.children.length > 0
},
},
})
return (
<FileContext.Provider
value={{
send,
state,
context: state.context, // just a convenience, can remove if we need to save on memory
}}
>
{children}
</FileContext.Provider>
)
}
export default FileMachineProvider