Chore: separate out projectMachine from Home route (#4109)

* Rename `homeMachine` and accessories to `projectsMachine`

* Separate out `/home` route from `projectsMachine`

* Add logic to navigate out from deleted or renamed project

* Show a warning in the command palette for deleting a project

* Make it navigate when you create a project

* Update "New project" button to use command bar flow
Closes #2585

* More explicit warning message text

* Make projects watching code not run in web

* Tests first version: nested loops

* Tests second version: flattened

* Remove console logs

* Fix tsc

* @jtran feedback, use the type guard util

* A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu-latest)

* A snapshot a day keeps the bugs away! 📷🐛 (OS: windows-latest)

* A snapshot a day keeps the bugs away! 📷🐛 (OS: windows-latest)

* Fix tests that relied on one-click, no-navigation project creation

* Revert "A snapshot a day keeps the bugs away! 📷🐛 (OS: windows-latest)"

This reverts commit 7545b61b49.

* Revert "A snapshot a day keeps the bugs away! 📷🐛 (OS: windows-latest)"

This reverts commit 3d2e48732c.

* Add a mask to the state indicator to client-side scale test

* A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu-latest)

* A snapshot a day keeps the bugs away! 📷🐛 (OS: windows-latest)

* A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu-latest)

* Fix lint

* Fix tsc

* Fix a couple stray tests that still relied on the old way of creating projects

* De-flake another text that could be thrown off by toast-based selectors

* FMT

* Dumb test error because I was rushing

* A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu-latest)

* Ahhh more flaky toasts, they're everywhere!

* A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu-latest)

* A snapshot a day keeps the bugs away! 📷🐛 (OS: windows-latest)

* A snapshot a day keeps the bugs away! 📷🐛 (OS: windows-latest)

* Re-run CI

* A snapshot a day keeps the bugs away! 📷🐛 (OS: windows-latest)

* Re-run CI

* Fix one test added since this PR was made

* Fix a few tests that failed due to changes since PR was made

* Prevent double selector issue in Ubuntu test

* Prevent *a different* double selector issue

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
This commit is contained in:
Frank Noirot
2024-10-28 16:18:06 -04:00
committed by GitHub
parent 05610bb0f3
commit 550c8ae165
20 changed files with 700 additions and 280 deletions

View File

@ -1,60 +1,42 @@
import { FormEvent, useEffect, useRef, useState } from 'react'
import {
getNextProjectIndex,
interpolateProjectNameWithIndex,
doesProjectNameNeedInterpolated,
} from 'lib/desktopFS'
import { ActionButton } from 'components/ActionButton'
import { toast } from 'react-hot-toast'
import { AppHeader } from 'components/AppHeader'
import ProjectCard from 'components/ProjectCard/ProjectCard'
import { useNavigate, useSearchParams } from 'react-router-dom'
import { Link } from 'react-router-dom'
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 {
getNextSearchParams,
getSortFunction,
getSortIcon,
} from '../lib/sorting'
import useStateMachineCommands from '../hooks/useStateMachineCommands'
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
import { useCommandsContext } from 'hooks/useCommandsContext'
import { homeCommandBarConfig } from 'lib/commandBarConfigs/homeCommandConfig'
import { useHotkeys } from 'react-hotkeys-hook'
import { isDesktop } from 'lib/isDesktop'
import { kclManager } from 'lib/singletons'
import { useLspContext } from 'components/LspProvider'
import { useRefreshSettings } from 'hooks/useRefreshSettings'
import { LowerRightControls } from 'components/LowerRightControls'
import {
createNewProjectDirectory,
listProjects,
renameProjectDirectory,
} from 'lib/desktop'
import { ProjectSearchBar, useProjectSearch } from 'components/ProjectSearchBar'
import { Project } from 'lib/project'
import { useFileSystemWatcher } from 'hooks/useFileSystemWatcher'
import { useProjectsLoader } from 'hooks/useProjectsLoader'
import { useProjectsContext } from 'hooks/useProjectsContext'
import { useCommandsContext } from 'hooks/useCommandsContext'
// This route only opens in the desktop context for now,
// as defined in Router.tsx, so we can use the desktop APIs and types.
const Home = () => {
const { state, send } = useProjectsContext()
const { commandBarSend } = useCommandsContext()
const [projectsLoaderTrigger, setProjectsLoaderTrigger] = useState(0)
const { projectPaths, projectsDir } = useProjectsLoader([
projectsLoaderTrigger,
])
const { projectsDir } = useProjectsLoader([projectsLoaderTrigger])
useRefreshSettings(PATHS.HOME + 'SETTINGS')
const { commandBarSend } = useCommandsContext()
const navigate = useNavigate()
const {
settings: { context: settings },
} = useSettingsAuthContext()
const { onProjectOpen } = useLspContext()
// Cancel all KCL executions while on the home page
useEffect(() => {
@ -73,107 +55,6 @@ const Home = () => {
)
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.
useFileSystemWatcher(
async () => {
@ -182,21 +63,13 @@ const Home = () => {
projectsDir ? [projectsDir] : []
)
const { projects } = state.context
const projects = state?.context.projects ?? []
const [searchParams, setSearchParams] = useSearchParams()
const { searchResults, query, setQuery } = useProjectSearch(projects)
const sort = searchParams.get('sort_by') ?? 'modified:desc'
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
// when the settings change
useEffect(() => {
@ -247,7 +120,16 @@ const Home = () => {
<ActionButton
Element="button"
onClick={() =>
send({ type: 'Create project', data: { name: '' } })
commandBarSend({
type: 'Find and select command',
data: {
groupId: 'projects',
name: 'Create project',
argDefaultValues: {
name: settings.projects.defaultProjectName.current,
},
},
})
}
className="group !bg-primary !text-chalkboard-10 !border-primary hover:shadow-inner hover:hue-rotate-15"
iconStart={{
@ -326,7 +208,7 @@ const Home = () => {
data-testid="home-section"
className="flex-1 overflow-y-auto pr-2 pb-24"
>
{state.matches('Reading projects') ? (
{state?.matches('Reading projects') ? (
<Loading>Loading your Projects...</Loading>
) : (
<>