Separate out /home
route from projectsMachine
This commit is contained in:
@ -42,6 +42,7 @@ import { coreDump } from 'lang/wasm'
|
|||||||
import { useMemo } from 'react'
|
import { useMemo } from 'react'
|
||||||
import { AppStateProvider } from 'AppState'
|
import { AppStateProvider } from 'AppState'
|
||||||
import { reportRejection } from 'lib/trap'
|
import { reportRejection } from 'lib/trap'
|
||||||
|
import { ProjectsContextProvider } from 'components/ProjectsContextProvider'
|
||||||
|
|
||||||
const createRouter = isDesktop() ? createHashRouter : createBrowserRouter
|
const createRouter = isDesktop() ? createHashRouter : createBrowserRouter
|
||||||
|
|
||||||
@ -55,11 +56,13 @@ const router = createRouter([
|
|||||||
<CommandBarProvider>
|
<CommandBarProvider>
|
||||||
<SettingsAuthProvider>
|
<SettingsAuthProvider>
|
||||||
<LspProvider>
|
<LspProvider>
|
||||||
<KclContextProvider>
|
<ProjectsContextProvider>
|
||||||
<AppStateProvider>
|
<KclContextProvider>
|
||||||
<Outlet />
|
<AppStateProvider>
|
||||||
</AppStateProvider>
|
<Outlet />
|
||||||
</KclContextProvider>
|
</AppStateProvider>
|
||||||
|
</KclContextProvider>
|
||||||
|
</ProjectsContextProvider>
|
||||||
</LspProvider>
|
</LspProvider>
|
||||||
</SettingsAuthProvider>
|
</SettingsAuthProvider>
|
||||||
</CommandBarProvider>
|
</CommandBarProvider>
|
||||||
|
197
src/components/ProjectsContextProvider.tsx
Normal file
197
src/components/ProjectsContextProvider.tsx
Normal file
@ -0,0 +1,197 @@
|
|||||||
|
import { useMachine } from '@xstate/react'
|
||||||
|
import { useCommandsContext } from 'hooks/useCommandsContext'
|
||||||
|
import { useFileSystemWatcher } from 'hooks/useFileSystemWatcher'
|
||||||
|
import { useProjectsLoader } from 'hooks/useProjectsLoader'
|
||||||
|
import { projectsMachine } from 'machines/projectsMachine'
|
||||||
|
import { createContext, useEffect, useState } from 'react'
|
||||||
|
import { Actor, AnyStateMachine, fromPromise, Prop, StateFrom } from 'xstate'
|
||||||
|
import { useLspContext } from './LspProvider'
|
||||||
|
import toast from 'react-hot-toast'
|
||||||
|
import { useNavigate } from 'react-router-dom'
|
||||||
|
import { PATHS } from 'lib/paths'
|
||||||
|
import {
|
||||||
|
createNewProjectDirectory,
|
||||||
|
listProjects,
|
||||||
|
renameProjectDirectory,
|
||||||
|
} from 'lib/desktop'
|
||||||
|
import {
|
||||||
|
getNextProjectIndex,
|
||||||
|
interpolateProjectNameWithIndex,
|
||||||
|
doesProjectNameNeedInterpolated,
|
||||||
|
} from 'lib/desktopFS'
|
||||||
|
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
||||||
|
import useStateMachineCommands from 'hooks/useStateMachineCommands'
|
||||||
|
import { projectsCommandBarConfig } from 'lib/commandBarConfigs/projectsCommandConfig'
|
||||||
|
|
||||||
|
type MachineContext<T extends AnyStateMachine> = {
|
||||||
|
state: StateFrom<T>
|
||||||
|
send: Prop<Actor<T>, 'send'>
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ProjectsMachineContext = createContext(
|
||||||
|
{} as MachineContext<typeof projectsMachine>
|
||||||
|
)
|
||||||
|
|
||||||
|
export const ProjectsContextProvider = ({
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode
|
||||||
|
}) => {
|
||||||
|
const navigate = useNavigate()
|
||||||
|
const { commandBarSend } = useCommandsContext()
|
||||||
|
const { onProjectOpen } = useLspContext()
|
||||||
|
const {
|
||||||
|
settings: { context: settings },
|
||||||
|
} = useSettingsAuthContext()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
console.log(
|
||||||
|
'project directory changed',
|
||||||
|
settings.app.projectDirectory.current
|
||||||
|
)
|
||||||
|
}, [settings.app.projectDirectory.current])
|
||||||
|
|
||||||
|
const [projectsLoaderTrigger, setProjectsLoaderTrigger] = useState(0)
|
||||||
|
const { projectPaths, projectsDir } = useProjectsLoader([
|
||||||
|
projectsLoaderTrigger,
|
||||||
|
])
|
||||||
|
|
||||||
|
// Re-read projects listing if the projectDir has any updates.
|
||||||
|
useFileSystemWatcher(
|
||||||
|
() => {
|
||||||
|
setProjectsLoaderTrigger(projectsLoaderTrigger + 1)
|
||||||
|
},
|
||||||
|
projectsDir ? [projectsDir] : []
|
||||||
|
)
|
||||||
|
|
||||||
|
const [state, send, actor] = useMachine(
|
||||||
|
projectsMachine.provide({
|
||||||
|
actions: {
|
||||||
|
navigateToProject: ({ context, event }) => {
|
||||||
|
if ('data' in event && event.data && 'name' in event.data) {
|
||||||
|
let projectPath =
|
||||||
|
context.defaultDirectory +
|
||||||
|
window.electron.path.sep +
|
||||||
|
event.data.name
|
||||||
|
onProjectOpen(
|
||||||
|
{
|
||||||
|
name: event.data.name,
|
||||||
|
path: projectPath,
|
||||||
|
},
|
||||||
|
null
|
||||||
|
)
|
||||||
|
commandBarSend({ type: 'Close' })
|
||||||
|
const newPathName = `${PATHS.FILE}/${encodeURIComponent(
|
||||||
|
projectPath
|
||||||
|
)}`
|
||||||
|
console.log('navigating to', newPathName)
|
||||||
|
console.log('defaultDirectory is', context.defaultDirectory)
|
||||||
|
navigate(newPathName)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
toastSuccess: ({ event }) =>
|
||||||
|
toast.success(
|
||||||
|
('data' in event && typeof event.data === 'string' && event.data) ||
|
||||||
|
('output' in event &&
|
||||||
|
typeof event.output === 'string' &&
|
||||||
|
event.output) ||
|
||||||
|
''
|
||||||
|
),
|
||||||
|
toastError: ({ event }) =>
|
||||||
|
toast.error(
|
||||||
|
('data' in event && typeof event.data === 'string' && event.data) ||
|
||||||
|
('output' in event &&
|
||||||
|
typeof event.output === 'string' &&
|
||||||
|
event.output) ||
|
||||||
|
''
|
||||||
|
),
|
||||||
|
},
|
||||||
|
actors: {
|
||||||
|
readProjects: fromPromise(() => listProjects()),
|
||||||
|
createProject: fromPromise(async ({ input }) => {
|
||||||
|
let name = (
|
||||||
|
input && 'name' in input && input.name
|
||||||
|
? input.name
|
||||||
|
: settings.projects.defaultProjectName.current
|
||||||
|
).trim()
|
||||||
|
|
||||||
|
if (doesProjectNameNeedInterpolated(name)) {
|
||||||
|
const nextIndex = getNextProjectIndex(name, input.projects)
|
||||||
|
name = interpolateProjectNameWithIndex(name, nextIndex)
|
||||||
|
}
|
||||||
|
|
||||||
|
await createNewProjectDirectory(name)
|
||||||
|
|
||||||
|
return `Successfully created "${name}"`
|
||||||
|
}),
|
||||||
|
renameProject: fromPromise(async ({ input }) => {
|
||||||
|
const {
|
||||||
|
oldName,
|
||||||
|
newName,
|
||||||
|
defaultProjectName,
|
||||||
|
defaultDirectory,
|
||||||
|
projects,
|
||||||
|
} = input
|
||||||
|
let name = newName ? newName : defaultProjectName
|
||||||
|
if (doesProjectNameNeedInterpolated(name)) {
|
||||||
|
const nextIndex = await getNextProjectIndex(name, projects)
|
||||||
|
name = interpolateProjectNameWithIndex(name, nextIndex)
|
||||||
|
}
|
||||||
|
|
||||||
|
await renameProjectDirectory(
|
||||||
|
window.electron.path.join(defaultDirectory, oldName),
|
||||||
|
name
|
||||||
|
)
|
||||||
|
return `Successfully renamed "${oldName}" to "${name}"`
|
||||||
|
}),
|
||||||
|
deleteProject: fromPromise(async ({ input }) => {
|
||||||
|
await window.electron.rm(
|
||||||
|
window.electron.path.join(input.defaultDirectory, input.name),
|
||||||
|
{
|
||||||
|
recursive: true,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return `Successfully deleted "${input.name}"`
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
guards: {
|
||||||
|
'Has at least 1 project': ({ event }) => {
|
||||||
|
if (event.type !== 'xstate.done.actor.read-projects') return false
|
||||||
|
console.log(`from has at least 1 project: ${event.output.length}`)
|
||||||
|
return event.output.length ? event.output.length >= 1 : false
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
input: {
|
||||||
|
projects: projectPaths,
|
||||||
|
defaultProjectName: settings.projects.defaultProjectName.current,
|
||||||
|
defaultDirectory: settings.app.projectDirectory.current,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
send({ type: 'Read projects', data: {} })
|
||||||
|
}, [projectPaths])
|
||||||
|
|
||||||
|
// register all project-related command palette commands
|
||||||
|
useStateMachineCommands({
|
||||||
|
machineId: 'projects',
|
||||||
|
send,
|
||||||
|
state,
|
||||||
|
commandBarConfig: projectsCommandBarConfig,
|
||||||
|
actor,
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ProjectsMachineContext.Provider
|
||||||
|
value={{
|
||||||
|
state,
|
||||||
|
send,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</ProjectsMachineContext.Provider>
|
||||||
|
)
|
||||||
|
}
|
6
src/hooks/useProjectsContext.ts
Normal file
6
src/hooks/useProjectsContext.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import { ProjectsMachineContext } from 'components/ProjectsContextProvider'
|
||||||
|
import { useContext } from 'react'
|
||||||
|
|
||||||
|
export const useProjectsContext = () => {
|
||||||
|
return useContext(ProjectsMachineContext)
|
||||||
|
}
|
@ -1,60 +1,40 @@
|
|||||||
import { FormEvent, useEffect, useRef, useState } from 'react'
|
import { FormEvent, useEffect, useRef, useState } from 'react'
|
||||||
import {
|
|
||||||
getNextProjectIndex,
|
|
||||||
interpolateProjectNameWithIndex,
|
|
||||||
doesProjectNameNeedInterpolated,
|
|
||||||
} from 'lib/desktopFS'
|
|
||||||
import { ActionButton } from 'components/ActionButton'
|
import { ActionButton } from 'components/ActionButton'
|
||||||
import { toast } from 'react-hot-toast'
|
|
||||||
import { AppHeader } from 'components/AppHeader'
|
import { AppHeader } from 'components/AppHeader'
|
||||||
import ProjectCard from 'components/ProjectCard/ProjectCard'
|
import ProjectCard from 'components/ProjectCard/ProjectCard'
|
||||||
import { useNavigate, useSearchParams } from 'react-router-dom'
|
import { useNavigate, useSearchParams } from 'react-router-dom'
|
||||||
import { Link } from 'react-router-dom'
|
import { Link } from 'react-router-dom'
|
||||||
import Loading from 'components/Loading'
|
import Loading from 'components/Loading'
|
||||||
import { useMachine } from '@xstate/react'
|
|
||||||
import { homeMachine } from '../machines/homeMachine'
|
|
||||||
import { fromPromise } from 'xstate'
|
|
||||||
import { PATHS } from 'lib/paths'
|
import { PATHS } from 'lib/paths'
|
||||||
import {
|
import {
|
||||||
getNextSearchParams,
|
getNextSearchParams,
|
||||||
getSortFunction,
|
getSortFunction,
|
||||||
getSortIcon,
|
getSortIcon,
|
||||||
} from '../lib/sorting'
|
} from '../lib/sorting'
|
||||||
import useStateMachineCommands from '../hooks/useStateMachineCommands'
|
|
||||||
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
||||||
import { useCommandsContext } from 'hooks/useCommandsContext'
|
|
||||||
import { homeCommandBarConfig } from 'lib/commandBarConfigs/homeCommandConfig'
|
|
||||||
import { useHotkeys } from 'react-hotkeys-hook'
|
import { useHotkeys } from 'react-hotkeys-hook'
|
||||||
import { isDesktop } from 'lib/isDesktop'
|
import { isDesktop } from 'lib/isDesktop'
|
||||||
import { kclManager } from 'lib/singletons'
|
import { kclManager } from 'lib/singletons'
|
||||||
import { useLspContext } from 'components/LspProvider'
|
|
||||||
import { useRefreshSettings } from 'hooks/useRefreshSettings'
|
import { useRefreshSettings } from 'hooks/useRefreshSettings'
|
||||||
import { LowerRightControls } from 'components/LowerRightControls'
|
import { LowerRightControls } from 'components/LowerRightControls'
|
||||||
import {
|
|
||||||
createNewProjectDirectory,
|
|
||||||
listProjects,
|
|
||||||
renameProjectDirectory,
|
|
||||||
} from 'lib/desktop'
|
|
||||||
import { ProjectSearchBar, useProjectSearch } from 'components/ProjectSearchBar'
|
import { ProjectSearchBar, useProjectSearch } from 'components/ProjectSearchBar'
|
||||||
import { Project } from 'lib/project'
|
import { Project } from 'lib/project'
|
||||||
import { useFileSystemWatcher } from 'hooks/useFileSystemWatcher'
|
import { useFileSystemWatcher } from 'hooks/useFileSystemWatcher'
|
||||||
import { useProjectsLoader } from 'hooks/useProjectsLoader'
|
import { useProjectsLoader } from 'hooks/useProjectsLoader'
|
||||||
|
import { useProjectsContext } from 'hooks/useProjectsContext'
|
||||||
|
|
||||||
// This route only opens in the desktop context for now,
|
// This route only opens in the desktop context for now,
|
||||||
// as defined in Router.tsx, so we can use the desktop APIs and types.
|
// as defined in Router.tsx, so we can use the desktop APIs and types.
|
||||||
const Home = () => {
|
const Home = () => {
|
||||||
|
const { state, send } = useProjectsContext()
|
||||||
const [projectsLoaderTrigger, setProjectsLoaderTrigger] = useState(0)
|
const [projectsLoaderTrigger, setProjectsLoaderTrigger] = useState(0)
|
||||||
const { projectPaths, projectsDir } = useProjectsLoader([
|
const { projectsDir } = useProjectsLoader([projectsLoaderTrigger])
|
||||||
projectsLoaderTrigger,
|
|
||||||
])
|
|
||||||
|
|
||||||
useRefreshSettings(PATHS.HOME + 'SETTINGS')
|
useRefreshSettings(PATHS.HOME + 'SETTINGS')
|
||||||
const { commandBarSend } = useCommandsContext()
|
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const {
|
const {
|
||||||
settings: { context: settings },
|
settings: { context: settings },
|
||||||
} = useSettingsAuthContext()
|
} = useSettingsAuthContext()
|
||||||
const { onProjectOpen } = useLspContext()
|
|
||||||
|
|
||||||
// Cancel all KCL executions while on the home page
|
// Cancel all KCL executions while on the home page
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -73,107 +53,6 @@ const Home = () => {
|
|||||||
)
|
)
|
||||||
const ref = useRef<HTMLDivElement>(null)
|
const ref = useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
const [state, send, actor] = useMachine(
|
|
||||||
homeMachine.provide({
|
|
||||||
actions: {
|
|
||||||
navigateToProject: ({ context, event }) => {
|
|
||||||
if ('data' in event && event.data && 'name' in event.data) {
|
|
||||||
let projectPath =
|
|
||||||
context.defaultDirectory +
|
|
||||||
window.electron.path.sep +
|
|
||||||
event.data.name
|
|
||||||
onProjectOpen(
|
|
||||||
{
|
|
||||||
name: event.data.name,
|
|
||||||
path: projectPath,
|
|
||||||
},
|
|
||||||
null
|
|
||||||
)
|
|
||||||
commandBarSend({ type: 'Close' })
|
|
||||||
navigate(`${PATHS.FILE}/${encodeURIComponent(projectPath)}`)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
toastSuccess: ({ event }) =>
|
|
||||||
toast.success(
|
|
||||||
('data' in event && typeof event.data === 'string' && event.data) ||
|
|
||||||
('output' in event &&
|
|
||||||
typeof event.output === 'string' &&
|
|
||||||
event.output) ||
|
|
||||||
''
|
|
||||||
),
|
|
||||||
toastError: ({ event }) =>
|
|
||||||
toast.error(
|
|
||||||
('data' in event && typeof event.data === 'string' && event.data) ||
|
|
||||||
('output' in event &&
|
|
||||||
typeof event.output === 'string' &&
|
|
||||||
event.output) ||
|
|
||||||
''
|
|
||||||
),
|
|
||||||
},
|
|
||||||
actors: {
|
|
||||||
readProjects: fromPromise(() => listProjects()),
|
|
||||||
createProject: fromPromise(async ({ input }) => {
|
|
||||||
let name = (
|
|
||||||
input && 'name' in input && input.name
|
|
||||||
? input.name
|
|
||||||
: settings.projects.defaultProjectName.current
|
|
||||||
).trim()
|
|
||||||
|
|
||||||
if (doesProjectNameNeedInterpolated(name)) {
|
|
||||||
const nextIndex = getNextProjectIndex(name, projects)
|
|
||||||
name = interpolateProjectNameWithIndex(name, nextIndex)
|
|
||||||
}
|
|
||||||
|
|
||||||
await createNewProjectDirectory(name)
|
|
||||||
|
|
||||||
return `Successfully created "${name}"`
|
|
||||||
}),
|
|
||||||
renameProject: fromPromise(async ({ input }) => {
|
|
||||||
const { oldName, newName, defaultProjectName, defaultDirectory } =
|
|
||||||
input
|
|
||||||
let name = newName ? newName : defaultProjectName
|
|
||||||
if (doesProjectNameNeedInterpolated(name)) {
|
|
||||||
const nextIndex = await getNextProjectIndex(name, projects)
|
|
||||||
name = interpolateProjectNameWithIndex(name, nextIndex)
|
|
||||||
}
|
|
||||||
|
|
||||||
await renameProjectDirectory(
|
|
||||||
window.electron.path.join(defaultDirectory, oldName),
|
|
||||||
name
|
|
||||||
)
|
|
||||||
return `Successfully renamed "${oldName}" to "${name}"`
|
|
||||||
}),
|
|
||||||
deleteProject: fromPromise(async ({ input }) => {
|
|
||||||
await window.electron.rm(
|
|
||||||
window.electron.path.join(input.defaultDirectory, input.name),
|
|
||||||
{
|
|
||||||
recursive: true,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
return `Successfully deleted "${input.name}"`
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
guards: {
|
|
||||||
'Has at least 1 project': ({ event }) => {
|
|
||||||
if (event.type !== 'xstate.done.actor.read-projects') return false
|
|
||||||
console.log(`from has at least 1 project: ${event.output.length}`)
|
|
||||||
return event.output.length ? event.output.length >= 1 : false
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
{
|
|
||||||
input: {
|
|
||||||
projects: projectPaths,
|
|
||||||
defaultProjectName: settings.projects.defaultProjectName.current,
|
|
||||||
defaultDirectory: settings.app.projectDirectory.current,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
send({ type: 'Read projects', data: {} })
|
|
||||||
}, [projectPaths])
|
|
||||||
|
|
||||||
// Re-read projects listing if the projectDir has any updates.
|
// Re-read projects listing if the projectDir has any updates.
|
||||||
useFileSystemWatcher(
|
useFileSystemWatcher(
|
||||||
() => {
|
() => {
|
||||||
@ -189,14 +68,6 @@ const Home = () => {
|
|||||||
|
|
||||||
const isSortByModified = sort?.includes('modified') || !sort || sort === null
|
const isSortByModified = sort?.includes('modified') || !sort || sort === null
|
||||||
|
|
||||||
useStateMachineCommands({
|
|
||||||
machineId: 'home',
|
|
||||||
send,
|
|
||||||
state,
|
|
||||||
commandBarConfig: homeCommandBarConfig,
|
|
||||||
actor,
|
|
||||||
})
|
|
||||||
|
|
||||||
// Update the default project name and directory in the home machine
|
// Update the default project name and directory in the home machine
|
||||||
// when the settings change
|
// when the settings change
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
Reference in New Issue
Block a user