* chore: skeleton to detect read write directories and if we have access to notify user * chore: adding buttont to easily change project directory * chore: cleaning up home page error bar layout and button * fix: adding clearer comments * fix: ugly console debugging but I need to save off progress * fix: removing project dir check on empty string * fix: debug progress to save off listProjects once. Still bugged... * fix: more hard coded debugging to get project loading optimizted * fix: yarp, we got another one bois * fix: cleaning up code * fix: massive bug comment to warn devs about chokidar bugs * fix: returning error instead of throwing * fix: cleaning up PR * fix: fixed loading the projects when the project directory changes * fix: remove testing code * fix: only skip directories if you can access the project directory since we don't need to view them * fix: unit tests, turning off noisey localhost vitest garbage * A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores) * A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores) * fix: deleted testing state --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> Co-authored-by: Pierre Jacquier <pierrejacquier39@gmail.com> Co-authored-by: Pierre Jacquier <pierre@zoo.dev>
294 lines
10 KiB
TypeScript
294 lines
10 KiB
TypeScript
import { FormEvent, useEffect, useRef, useState } from 'react'
|
|
import { ActionButton } from 'components/ActionButton'
|
|
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 { toast } from 'react-hot-toast'
|
|
import Loading from 'components/Loading'
|
|
import { PATHS } from 'lib/paths'
|
|
import {
|
|
getNextSearchParams,
|
|
getSortFunction,
|
|
getSortIcon,
|
|
} from '../lib/sorting'
|
|
import { useHotkeys } from 'react-hotkeys-hook'
|
|
import { isDesktop } from 'lib/isDesktop'
|
|
import { kclManager } from 'lib/singletons'
|
|
import { LowerRightControls } from 'components/LowerRightControls'
|
|
import { ProjectSearchBar, useProjectSearch } from 'components/ProjectSearchBar'
|
|
import { Project } from 'lib/project'
|
|
import { markOnce } from 'lib/performance'
|
|
import { useProjectsContext } from 'hooks/useProjectsContext'
|
|
import { commandBarActor } from 'machines/commandBarMachine'
|
|
import { useCreateFileLinkQuery } from 'hooks/useCreateFileLinkQueryWatcher'
|
|
import { useSettings } from 'machines/appMachine'
|
|
import { reportRejection } from 'lib/trap'
|
|
|
|
// 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 [readWriteProjectDir, setReadWriteProjectDir] = useState<{
|
|
value: boolean
|
|
error: unknown
|
|
}>({
|
|
value: true,
|
|
error: undefined,
|
|
})
|
|
|
|
// Keep a lookout for a URL query string that invokes the 'import file from URL' command
|
|
useCreateFileLinkQuery((argDefaultValues) => {
|
|
commandBarActor.send({
|
|
type: 'Find and select command',
|
|
data: {
|
|
groupId: 'projects',
|
|
name: 'Import file from URL',
|
|
argDefaultValues,
|
|
},
|
|
})
|
|
})
|
|
|
|
const navigate = useNavigate()
|
|
const settings = useSettings()
|
|
|
|
// Cancel all KCL executions while on the home page
|
|
useEffect(() => {
|
|
markOnce('code/didLoadHome')
|
|
kclManager.cancelAllExecutions()
|
|
}, [])
|
|
|
|
useHotkeys('backspace', (e) => {
|
|
e.preventDefault()
|
|
})
|
|
useHotkeys(
|
|
isDesktop() ? 'mod+,' : 'shift+mod+,',
|
|
() => navigate(PATHS.HOME + PATHS.SETTINGS),
|
|
{
|
|
splitKey: '|',
|
|
}
|
|
)
|
|
const ref = useRef<HTMLDivElement>(null)
|
|
|
|
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
|
|
|
|
// Update the default project name and directory in the home machine
|
|
// when the settings change
|
|
useEffect(() => {
|
|
send({
|
|
type: 'assign',
|
|
data: {
|
|
defaultProjectName: settings.projects.defaultProjectName.current,
|
|
defaultDirectory: settings.app.projectDirectory.current,
|
|
},
|
|
})
|
|
|
|
// Must be a truthy string, not '' or null or undefined
|
|
if (settings.app.projectDirectory.current) {
|
|
window.electron
|
|
.canReadWriteDirectory(settings.app.projectDirectory.current)
|
|
.then((res) => {
|
|
setReadWriteProjectDir(res)
|
|
})
|
|
.catch(reportRejection)
|
|
}
|
|
}, [
|
|
settings.app.projectDirectory.current,
|
|
settings.projects.defaultProjectName.current,
|
|
send,
|
|
])
|
|
|
|
async function handleRenameProject(
|
|
e: FormEvent<HTMLFormElement>,
|
|
project: Project
|
|
) {
|
|
const { newProjectName } = Object.fromEntries(
|
|
new FormData(e.target as HTMLFormElement)
|
|
)
|
|
|
|
if (typeof newProjectName === 'string' && newProjectName.startsWith('.')) {
|
|
toast.error('Project names cannot start with a dot (.)')
|
|
return
|
|
}
|
|
|
|
if (newProjectName !== project.name) {
|
|
send({
|
|
type: 'Rename project',
|
|
data: { oldName: project.name, newName: newProjectName as string },
|
|
})
|
|
}
|
|
}
|
|
|
|
async function handleDeleteProject(project: Project) {
|
|
send({
|
|
type: 'Delete project',
|
|
data: { name: project.name || '' },
|
|
})
|
|
}
|
|
/** Type narrowing function of unknown error to a string */
|
|
function errorMessage(error: unknown): string {
|
|
if (error != undefined && error instanceof Error) {
|
|
return error.message
|
|
} else if (error && typeof error === 'object') {
|
|
return JSON.stringify(error)
|
|
} else if (typeof error === 'string') {
|
|
return error
|
|
} else {
|
|
return 'Unknown error'
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div className="relative flex flex-col h-screen overflow-hidden" ref={ref}>
|
|
<AppHeader showToolbar={false} />
|
|
<div className="w-full flex flex-col overflow-hidden max-w-5xl px-4 mx-auto mt-24 lg:px-2">
|
|
<section>
|
|
<div className="flex justify-between items-center select-none">
|
|
<div className="flex gap-8 items-center">
|
|
<h1 className="text-3xl font-bold">Your Projects</h1>
|
|
<ActionButton
|
|
Element="button"
|
|
onClick={() =>
|
|
commandBarActor.send({
|
|
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={{
|
|
icon: 'plus',
|
|
bgClassName: '!bg-transparent rounded-sm',
|
|
iconClassName:
|
|
'!text-chalkboard-10 transition-transform group-active:rotate-90',
|
|
}}
|
|
data-testid="home-new-file"
|
|
>
|
|
Create project
|
|
</ActionButton>
|
|
</div>
|
|
<div className="flex gap-2 items-center">
|
|
<ProjectSearchBar setQuery={setQuery} />
|
|
<small>Sort by</small>
|
|
<ActionButton
|
|
Element="button"
|
|
data-testid="home-sort-by-name"
|
|
className={
|
|
'text-xs border-primary/10 ' +
|
|
(!sort.includes('name')
|
|
? 'text-chalkboard-80 dark:text-chalkboard-40'
|
|
: '')
|
|
}
|
|
onClick={() =>
|
|
setSearchParams(getNextSearchParams(sort, 'name'))
|
|
}
|
|
iconStart={{
|
|
icon: getSortIcon(sort, 'name'),
|
|
bgClassName: 'bg-transparent',
|
|
iconClassName: !sort.includes('name')
|
|
? '!text-chalkboard-90 dark:!text-chalkboard-30'
|
|
: '',
|
|
}}
|
|
>
|
|
Name
|
|
</ActionButton>
|
|
<ActionButton
|
|
Element="button"
|
|
data-testid="home-sort-by-modified"
|
|
className={
|
|
'text-xs border-primary/10 ' +
|
|
(!isSortByModified
|
|
? 'text-chalkboard-80 dark:text-chalkboard-40'
|
|
: '')
|
|
}
|
|
onClick={() =>
|
|
setSearchParams(getNextSearchParams(sort, 'modified'))
|
|
}
|
|
iconStart={{
|
|
icon: sort ? getSortIcon(sort, 'modified') : 'arrowDown',
|
|
bgClassName: 'bg-transparent',
|
|
iconClassName: !isSortByModified
|
|
? '!text-chalkboard-90 dark:!text-chalkboard-30'
|
|
: '',
|
|
}}
|
|
>
|
|
Last Modified
|
|
</ActionButton>
|
|
</div>
|
|
</div>
|
|
<p className="my-4 text-sm text-chalkboard-80 dark:text-chalkboard-30">
|
|
Loaded from{' '}
|
|
<Link
|
|
data-testid="project-directory-settings-link"
|
|
to={`${PATHS.HOME + PATHS.SETTINGS_USER}#projectDirectory`}
|
|
className="text-chalkboard-90 dark:text-chalkboard-20 underline underline-offset-2"
|
|
>
|
|
{settings.app.projectDirectory.current}
|
|
</Link>
|
|
.
|
|
</p>
|
|
{!readWriteProjectDir.value && (
|
|
<section>
|
|
<div className="flex items-center select-none">
|
|
<div className="flex gap-8 items-center justify-between grow bg-destroy-80 text-white py-1 px-4 my-2 rounded-sm grow">
|
|
<p className="">{errorMessage(readWriteProjectDir.error)}</p>
|
|
<Link
|
|
data-testid="project-directory-settings-link"
|
|
to={`${PATHS.HOME + PATHS.SETTINGS_USER}#projectDirectory`}
|
|
className="py-1 text-white underline underline-offset-2 text-sm"
|
|
>
|
|
Change Project Directory
|
|
</Link>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
)}
|
|
</section>
|
|
<section
|
|
data-testid="home-section"
|
|
className="flex-1 overflow-y-auto pr-2 pb-24"
|
|
>
|
|
{state?.matches('Reading projects') ? (
|
|
<Loading>Loading your Projects...</Loading>
|
|
) : (
|
|
<>
|
|
{searchResults.length > 0 ? (
|
|
<ul className="grid w-full grid-cols-4 gap-4">
|
|
{searchResults.sort(getSortFunction(sort)).map((project) => (
|
|
<ProjectCard
|
|
key={project.name}
|
|
project={project}
|
|
handleRenameProject={handleRenameProject}
|
|
handleDeleteProject={handleDeleteProject}
|
|
/>
|
|
))}
|
|
</ul>
|
|
) : (
|
|
<p className="p-4 my-8 border border-dashed rounded border-chalkboard-30 dark:border-chalkboard-70">
|
|
No Projects found
|
|
{projects.length === 0
|
|
? ', ready to make your first one?'
|
|
: ` with the search term "${query}"`}
|
|
</p>
|
|
)}
|
|
</>
|
|
)}
|
|
</section>
|
|
<LowerRightControls />
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export default Home
|