Rework home layout to have a sidebar (#6423)
* 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 * Rework home layout to have a sidebar fmt I think * Add two links to the bottom of the sidebar Mostly to visually anchor it * Tweak some style things * update test util whose locator needs to change * tsc and fmt * Stupid heading change broke the dang E2E tests * Make that heading locator a part of the home page fixture * pierremtb/new-snaps-for-frank (#6516) Co-authored-by: Pierre Jacquier <pierrejacquier39@gmail.com> --------- Co-authored-by: Kevin Nadro <kevin@zoo.dev> Co-authored-by: Kevin Nadro <nadr0@users.noreply.github.com> Co-authored-by: Pierre Jacquier <pierrejacquier39@gmail.com>
@ -247,7 +247,7 @@ export class ElectronZoo {
|
||||
}
|
||||
|
||||
if (!this.firstUrl) {
|
||||
await this.page.getByText('Your Projects').count()
|
||||
await this.page.getByRole('heading', { name: 'Projects' }).count()
|
||||
this.firstUrl = this.page.url()
|
||||
}
|
||||
|
||||
|
@ -92,6 +92,13 @@ export class HomePageFixture {
|
||||
}
|
||||
}
|
||||
|
||||
expectIsCurrentPage = async () => {
|
||||
await expect(this.page).toHaveURL(/.*home/)
|
||||
await expect(
|
||||
this.page.getByRole('heading', { name: 'Projects' })
|
||||
).toBeVisible()
|
||||
}
|
||||
|
||||
projectsLoaded = async () => {
|
||||
await expect(this.projectSection).not.toHaveText('Loading your Projects...')
|
||||
}
|
||||
|
@ -510,9 +510,7 @@ test('Restarting onboarding on desktop takes one attempt', async ({
|
||||
})
|
||||
|
||||
await test.step('Navigate into project', async () => {
|
||||
await expect(
|
||||
page.getByRole('heading', { name: 'Your Projects' })
|
||||
).toBeVisible()
|
||||
await expect(page.getByRole('heading', { name: 'Projects' })).toBeVisible()
|
||||
await expect(projectCard).toBeVisible()
|
||||
await projectCard.click()
|
||||
await u.waitForPageLoad()
|
||||
|
@ -723,7 +723,7 @@ test(
|
||||
test(
|
||||
'pressing "delete" on home screen should do nothing',
|
||||
{ tag: '@electron' },
|
||||
async ({ context, page }, testInfo) => {
|
||||
async ({ context, page, homePage }, testInfo) => {
|
||||
await context.folderSetupFn(async (dir) => {
|
||||
await fsp.mkdir(`${dir}/router-template-slate`, { recursive: true })
|
||||
await fsp.copyFile(
|
||||
@ -737,7 +737,7 @@ test(
|
||||
|
||||
await expect(page.getByText('router-template-slate')).toBeVisible()
|
||||
await expect(page.getByText('Loading your Projects...')).not.toBeVisible()
|
||||
await expect(page.getByText('Your Projects')).toBeVisible()
|
||||
await homePage.expectIsCurrentPage()
|
||||
|
||||
await page.keyboard.press('Delete')
|
||||
await page.waitForTimeout(200)
|
||||
@ -745,7 +745,7 @@ test(
|
||||
|
||||
// expect to still be on the home page
|
||||
await expect(page.getByText('router-template-slate')).toBeVisible()
|
||||
await expect(page.getByText('Your Projects')).toBeVisible()
|
||||
await homePage.expectIsCurrentPage()
|
||||
}
|
||||
)
|
||||
|
||||
|
@ -579,7 +579,7 @@ extrude002 = extrude(profile002, length = 150)
|
||||
|
||||
// Locators
|
||||
const projectsHeading = page.getByRole('heading', {
|
||||
name: 'Your projects',
|
||||
name: 'Projects',
|
||||
})
|
||||
const projectLink = page.getByRole('link', { name: 'bracket' })
|
||||
const networkHealthIndicator = page.getByTestId('network-toggle')
|
||||
|
Before Width: | Height: | Size: 56 KiB After Width: | Height: | Size: 56 KiB |
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 49 KiB |
Before Width: | Height: | Size: 56 KiB After Width: | Height: | Size: 56 KiB |
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 49 KiB |
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 48 KiB |
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 47 KiB |
Before Width: | Height: | Size: 43 KiB After Width: | Height: | Size: 43 KiB |
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 46 KiB |
Before Width: | Height: | Size: 52 KiB After Width: | Height: | Size: 52 KiB |
Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 58 KiB |
Before Width: | Height: | Size: 68 KiB After Width: | Height: | Size: 64 KiB |
Before Width: | Height: | Size: 74 KiB After Width: | Height: | Size: 74 KiB |
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 46 KiB |
Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 58 KiB |
Before Width: | Height: | Size: 133 KiB After Width: | Height: | Size: 133 KiB |
Before Width: | Height: | Size: 116 KiB After Width: | Height: | Size: 116 KiB |
Before Width: | Height: | Size: 65 KiB After Width: | Height: | Size: 65 KiB |
Before Width: | Height: | Size: 68 KiB After Width: | Height: | Size: 68 KiB |
Before Width: | Height: | Size: 64 KiB After Width: | Height: | Size: 64 KiB |
Before Width: | Height: | Size: 67 KiB After Width: | Height: | Size: 67 KiB |
Before Width: | Height: | Size: 67 KiB After Width: | Height: | Size: 67 KiB |
Before Width: | Height: | Size: 63 KiB After Width: | Height: | Size: 63 KiB |
@ -1014,7 +1014,7 @@ export async function createProject({
|
||||
|
||||
async function goToHomePageFromModeling(page: Page) {
|
||||
await page.getByTestId('app-logo').click()
|
||||
await expect(page.getByText('Your Projects')).toBeVisible()
|
||||
await expect(page.getByRole('heading', { name: 'Projects' })).toBeVisible()
|
||||
}
|
||||
|
||||
export function executorInputPath(fileName: string): string {
|
||||
|
@ -786,7 +786,7 @@ test.describe('Mocked Text-to-CAD API tests', { tag: ['@skipWin'] }, () => {
|
||||
test(
|
||||
'Home Page -> Text To CAD -> Existing Project -> Stay in home page -> Reject -> should delete single file',
|
||||
{ tag: '@electron' },
|
||||
async ({ context, page }, testInfo) => {
|
||||
async ({ homePage, page }, testInfo) => {
|
||||
const projectName = 'my-project-name'
|
||||
const prompt = '2x2x2 cube'
|
||||
await mockPageTextToCAD(page)
|
||||
@ -794,7 +794,7 @@ test.describe('Mocked Text-to-CAD API tests', { tag: ['@skipWin'] }, () => {
|
||||
// Create and navigate to the project then come home
|
||||
await createProject({ name: projectName, page, returnHome: true })
|
||||
|
||||
await expect(page.getByText('Your Projects')).toBeVisible()
|
||||
await homePage.expectIsCurrentPage()
|
||||
|
||||
await expect(page.getByText('1 file')).toBeVisible()
|
||||
|
||||
@ -829,7 +829,7 @@ test.describe('Mocked Text-to-CAD API tests', { tag: ['@skipWin'] }, () => {
|
||||
test(
|
||||
'Home Page -> Text To CAD -> Existing Project -> Stay in home page -> Accept -> should navigate to file',
|
||||
{ tag: '@electron' },
|
||||
async ({ context, page }, testInfo) => {
|
||||
async ({ homePage, page }, testInfo) => {
|
||||
const u = await getUtils(page)
|
||||
const projectName = 'my-project-name'
|
||||
const prompt = '2x2x2 cube'
|
||||
@ -838,7 +838,7 @@ test.describe('Mocked Text-to-CAD API tests', { tag: ['@skipWin'] }, () => {
|
||||
// Create and navigate to the project then come home
|
||||
await createProject({ name: projectName, page, returnHome: true })
|
||||
|
||||
await expect(page.getByText('Your Projects')).toBeVisible()
|
||||
await homePage.expectIsCurrentPage()
|
||||
|
||||
// open commands
|
||||
await page.getByTestId('command-bar-open-button').click()
|
||||
@ -880,7 +880,7 @@ test.describe('Mocked Text-to-CAD API tests', { tag: ['@skipWin'] }, () => {
|
||||
test(
|
||||
'Home Page -> Text To CAD -> New Project -> Navigate to the project -> Reject -> should go to home page',
|
||||
{ tag: '@electron' },
|
||||
async ({ context, page }, testInfo) => {
|
||||
async ({ homePage, page }, testInfo) => {
|
||||
const projectName = 'my-project-name'
|
||||
const prompt = '2x2x2 cube'
|
||||
await mockPageTextToCAD(page)
|
||||
@ -919,16 +919,14 @@ test.describe('Mocked Text-to-CAD API tests', { tag: ['@skipWin'] }, () => {
|
||||
await page.getByRole('button', { name: 'Reject' }).click()
|
||||
|
||||
// Make sure we went back home
|
||||
await expect(
|
||||
page.getByText('No Projects found, ready to make your first one?')
|
||||
).toBeVisible()
|
||||
await homePage.expectIsCurrentPage()
|
||||
}
|
||||
)
|
||||
|
||||
test(
|
||||
'Home Page -> Text To CAD -> New Project -> Navigate to the project -> Accept -> should stay in same file',
|
||||
{ tag: '@electron' },
|
||||
async ({ context, page }, testInfo) => {
|
||||
async ({ homePage, page }, testInfo) => {
|
||||
const projectName = 'my-project-name'
|
||||
const prompt = '2x2x2 cube'
|
||||
await mockPageTextToCAD(page)
|
||||
@ -971,7 +969,7 @@ test.describe('Mocked Text-to-CAD API tests', { tag: ['@skipWin'] }, () => {
|
||||
test(
|
||||
'Home Page -> Text To CAD -> Existing Project -> Navigate to the project -> Reject -> should load main.kcl',
|
||||
{ tag: '@electron' },
|
||||
async ({ context, page }, testInfo) => {
|
||||
async ({ homePage, page }, testInfo) => {
|
||||
const u = await getUtils(page)
|
||||
const projectName = 'my-project-name'
|
||||
const prompt = '2x2x2 cube'
|
||||
@ -980,7 +978,7 @@ test.describe('Mocked Text-to-CAD API tests', { tag: ['@skipWin'] }, () => {
|
||||
// Create and navigate to the project then come home
|
||||
await createProject({ name: projectName, page, returnHome: true })
|
||||
|
||||
await expect(page.getByText('Your Projects')).toBeVisible()
|
||||
await homePage.expectIsCurrentPage()
|
||||
|
||||
// open commands
|
||||
await page.getByTestId('command-bar-open-button').click()
|
||||
@ -1026,7 +1024,7 @@ test.describe('Mocked Text-to-CAD API tests', { tag: ['@skipWin'] }, () => {
|
||||
test(
|
||||
'Home Page -> Text To CAD -> Existing Project -> Navigate to the project -> Accept -> should load 2x2x2-cube.kcl',
|
||||
{ tag: '@electron' },
|
||||
async ({ context, page }, testInfo) => {
|
||||
async ({ homePage, page }, testInfo) => {
|
||||
const u = await getUtils(page)
|
||||
const projectName = 'my-project-name'
|
||||
const prompt = '2x2x2 cube'
|
||||
@ -1035,7 +1033,7 @@ test.describe('Mocked Text-to-CAD API tests', { tag: ['@skipWin'] }, () => {
|
||||
// Create and navigate to the project then come home
|
||||
await createProject({ name: projectName, page, returnHome: true })
|
||||
|
||||
await expect(page.getByText('Your Projects')).toBeVisible()
|
||||
await homePage.expectIsCurrentPage()
|
||||
|
||||
// open commands
|
||||
await page.getByTestId('command-bar-open-button').click()
|
||||
@ -1083,7 +1081,7 @@ test.describe('Mocked Text-to-CAD API tests', { tag: ['@skipWin'] }, () => {
|
||||
test(
|
||||
'Home Page -> Text To CAD -> New Project -> Navigate to different project -> Reject -> should stay in project',
|
||||
{ tag: '@electron' },
|
||||
async ({ context, page, homePage }, testInfo) => {
|
||||
async ({ homePage, page }, testInfo) => {
|
||||
const u = await getUtils(page)
|
||||
const projectName = 'my-project-name'
|
||||
const unrelatedProjectName = 'unrelated-project'
|
||||
@ -1097,7 +1095,7 @@ test.describe('Mocked Text-to-CAD API tests', { tag: ['@skipWin'] }, () => {
|
||||
returnHome: true,
|
||||
})
|
||||
|
||||
await expect(page.getByText('Your Projects')).toBeVisible()
|
||||
await homePage.expectIsCurrentPage()
|
||||
|
||||
// open commands
|
||||
await page.getByTestId('command-bar-open-button').click()
|
||||
@ -1160,7 +1158,7 @@ test.describe('Mocked Text-to-CAD API tests', { tag: ['@skipWin'] }, () => {
|
||||
test(
|
||||
'Home Page -> Text To CAD -> New Project -> Navigate to different project -> Accept -> should go to new project',
|
||||
{ tag: '@electron' },
|
||||
async ({ context, page, homePage }, testInfo) => {
|
||||
async ({ page, homePage }, testInfo) => {
|
||||
const u = await getUtils(page)
|
||||
const projectName = 'my-project-name'
|
||||
const unrelatedProjectName = 'unrelated-project'
|
||||
@ -1174,7 +1172,7 @@ test.describe('Mocked Text-to-CAD API tests', { tag: ['@skipWin'] }, () => {
|
||||
returnHome: true,
|
||||
})
|
||||
|
||||
await expect(page.getByText('Your Projects')).toBeVisible()
|
||||
await homePage.expectIsCurrentPage()
|
||||
|
||||
// open commands
|
||||
await page.getByTestId('command-bar-open-button').click()
|
||||
@ -1233,7 +1231,7 @@ test.describe('Mocked Text-to-CAD API tests', { tag: ['@skipWin'] }, () => {
|
||||
test(
|
||||
'Home Page -> Text To CAD -> Existing Project -> Navigate to different project -> Reject -> should stay in same project',
|
||||
{ tag: '@electron' },
|
||||
async ({ context, page, homePage }, testInfo) => {
|
||||
async ({ page, homePage }, testInfo) => {
|
||||
const u = await getUtils(page)
|
||||
const projectName = 'my-project-name'
|
||||
const unrelatedProjectName = 'unrelated-project'
|
||||
@ -1246,10 +1244,10 @@ test.describe('Mocked Text-to-CAD API tests', { tag: ['@skipWin'] }, () => {
|
||||
page,
|
||||
returnHome: true,
|
||||
})
|
||||
await expect(page.getByText('Your Projects')).toBeVisible()
|
||||
await homePage.expectIsCurrentPage()
|
||||
|
||||
await createProject({ name: projectName, page, returnHome: true })
|
||||
await expect(page.getByText('Your Projects')).toBeVisible()
|
||||
await homePage.expectIsCurrentPage()
|
||||
|
||||
// open commands
|
||||
await page.getByTestId('command-bar-open-button').click()
|
||||
@ -1308,7 +1306,7 @@ test.describe('Mocked Text-to-CAD API tests', { tag: ['@skipWin'] }, () => {
|
||||
test(
|
||||
'Home Page -> Text To CAD -> Existing Project -> Navigate to different project -> Accept -> should navigate to new project',
|
||||
{ tag: '@electron' },
|
||||
async ({ context, page, homePage }, testInfo) => {
|
||||
async ({ page, homePage }, testInfo) => {
|
||||
const u = await getUtils(page)
|
||||
const projectName = 'my-project-name'
|
||||
const unrelatedProjectName = 'unrelated-project'
|
||||
@ -1321,10 +1319,10 @@ test.describe('Mocked Text-to-CAD API tests', { tag: ['@skipWin'] }, () => {
|
||||
page,
|
||||
returnHome: true,
|
||||
})
|
||||
await expect(page.getByText('Your Projects')).toBeVisible()
|
||||
await homePage.expectIsCurrentPage()
|
||||
|
||||
await createProject({ name: projectName, page, returnHome: true })
|
||||
await expect(page.getByText('Your Projects')).toBeVisible()
|
||||
await homePage.expectIsCurrentPage()
|
||||
|
||||
// open commands
|
||||
await page.getByTestId('command-bar-open-button').click()
|
||||
|
@ -44,7 +44,7 @@ export type ActionButtonProps =
|
||||
| ActionButtonAsElement
|
||||
|
||||
export const ActionButton = forwardRef((props: ActionButtonProps, ref) => {
|
||||
const classNames = `action-button leading-[0.7] p-0 m-0 group mono text-xs leading-none flex items-center gap-2 rounded-sm border-solid border border-chalkboard-30 hover:border-chalkboard-40 enabled:dark:border-chalkboard-70 dark:hover:border-chalkboard-60 dark:bg-chalkboard-90/50 text-chalkboard-100 dark:text-chalkboard-10 ${
|
||||
const classNames = `action-button leading-[0.7] p-0 m-0 group text-xs leading-none flex items-center gap-2 rounded-sm border-solid border border-chalkboard-30 hover:border-chalkboard-40 enabled:dark:border-chalkboard-70 dark:hover:border-chalkboard-60 dark:bg-chalkboard-90/50 text-chalkboard-100 dark:text-chalkboard-10 ${
|
||||
props.iconStart
|
||||
? props.iconEnd
|
||||
? 'px-0' // No padding if both icons are present
|
||||
|
@ -84,7 +84,7 @@ function ProjectCard({
|
||||
return (
|
||||
<li
|
||||
{...props}
|
||||
className="group relative flex flex-col rounded-sm border border-primary/40 dark:border-chalkboard-80 hover:!border-primary"
|
||||
className="group relative flex flex-col rounded-sm border border-chalkboard-50 dark:border-chalkboard-80 hover:!border-primary"
|
||||
>
|
||||
<Link
|
||||
data-testid="project-link"
|
||||
@ -93,7 +93,7 @@ function ProjectCard({
|
||||
? `${PATHS.FILE}/${encodeURIComponent(project.default_file)}`
|
||||
: ''
|
||||
}
|
||||
className={`flex flex-col flex-1 !no-underline !text-chalkboard-110 dark:!text-chalkboard-10 min-h-[5em] divide-y divide-primary/40 dark:divide-chalkboard-80 ${
|
||||
className={`flex flex-col flex-1 !no-underline !text-chalkboard-110 dark:!text-chalkboard-10 min-h-[5em] divide-y divide-chalkboard-50 dark:divide-chalkboard-80 ${
|
||||
project.readWriteAccess
|
||||
? 'group-hover:!divide-primary group-hover:!hue-rotate-0'
|
||||
: 'cursor-not-allowed'
|
||||
|
@ -286,6 +286,11 @@ code {
|
||||
@apply bg-chalkboard-20 dark:bg-chalkboard-90;
|
||||
@apply border border-t-0 border-b-2 border-chalkboard-30 dark:border-chalkboard-80;
|
||||
}
|
||||
|
||||
.home-layout {
|
||||
@apply grid lg:grid-cols-3 xl:grid-cols-4 gap-4 lg:gap-x-16;
|
||||
grid-template-rows: auto 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
@layer utilities {
|
||||
|
@ -1,4 +1,4 @@
|
||||
import type { FormEvent } from 'react'
|
||||
import type { FormEvent, HTMLProps } from 'react'
|
||||
import { useEffect, useRef } from 'react'
|
||||
import { toast } from 'react-hot-toast'
|
||||
import { useHotkeys } from 'react-hotkeys-hook'
|
||||
@ -38,11 +38,16 @@ import {
|
||||
SystemIOMachineStates,
|
||||
} from '@src/machines/systemIO/utils'
|
||||
import type { WebContentSendPayload } from '@src/menu/channels'
|
||||
import { openExternalBrowserIfDesktop } from '@src/lib/openWindow'
|
||||
|
||||
type ReadWriteProjectState = {
|
||||
value: boolean
|
||||
error: unknown
|
||||
}
|
||||
|
||||
// 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 = useSystemIOState()
|
||||
const readWriteProjectDir = useCanReadWriteProjectDirectory()
|
||||
|
||||
// Only create the native file menus on desktop
|
||||
@ -117,9 +122,9 @@ const Home = () => {
|
||||
} else if (data.menuLabel === 'File.Preferences.Keybindings') {
|
||||
navigate(PATHS.HOME + PATHS.SETTINGS_KEYBINDINGS)
|
||||
} else if (data.menuLabel === 'File.Preferences.User default units') {
|
||||
navigate(PATHS.HOME + PATHS.SETTINGS_USER + '#defaultUnit')
|
||||
navigate(`${PATHS.HOME}${PATHS.SETTINGS_USER}#defaultUnit`)
|
||||
} else if (data.menuLabel === 'Edit.Change project directory') {
|
||||
navigate(PATHS.HOME + PATHS.SETTINGS_USER + '#projectDirectory')
|
||||
navigate(`${PATHS.HOME}${PATHS.SETTINGS_USER}#projectDirectory`)
|
||||
} else if (data.menuLabel === 'File.Sign out') {
|
||||
authActor.send({ type: 'Log out' })
|
||||
} else if (
|
||||
@ -136,7 +141,7 @@ const Home = () => {
|
||||
},
|
||||
})
|
||||
} else if (data.menuLabel === 'File.Preferences.Theme color') {
|
||||
navigate(PATHS.HOME + PATHS.SETTINGS_USER + '#themeColor')
|
||||
navigate(`${PATHS.HOME}${PATHS.SETTINGS_USER}#themeColor`)
|
||||
}
|
||||
}
|
||||
useMenuListener(cb)
|
||||
@ -162,9 +167,282 @@ const Home = () => {
|
||||
const [searchParams, setSearchParams] = useSearchParams()
|
||||
const { searchResults, query, setQuery } = useProjectSearch(projects)
|
||||
const sort = searchParams.get('sort_by') ?? 'modified:desc'
|
||||
const sidebarButtonClasses =
|
||||
'flex items-center p-2 gap-2 leading-tight border-transparent dark:border-transparent enabled:dark:border-transparent enabled:hover:border-primary/50 enabled:dark:hover:border-inherit active:border-primary dark:bg-transparent hover:bg-transparent'
|
||||
|
||||
return (
|
||||
<div className="relative flex flex-col h-screen overflow-hidden" ref={ref}>
|
||||
<AppHeader showToolbar={false} />
|
||||
<div className="overflow-hidden home-layout max-w-4xl xl:max-w-7xl mb-12 px-4 mx-auto mt-24 lg:px-0">
|
||||
<HomeHeader
|
||||
setQuery={setQuery}
|
||||
sort={sort}
|
||||
setSearchParams={setSearchParams}
|
||||
settings={settings}
|
||||
readWriteProjectDir={readWriteProjectDir}
|
||||
className="col-start-2 -col-end-1"
|
||||
/>
|
||||
<aside className="row-start-2 -row-end-1 flex flex-col justify-between">
|
||||
<ul className="flex flex-col">
|
||||
<li className="contents">
|
||||
<ActionButton
|
||||
Element="button"
|
||||
onClick={() =>
|
||||
commandBarActor.send({
|
||||
type: 'Find and select command',
|
||||
data: {
|
||||
groupId: 'projects',
|
||||
name: 'Create project',
|
||||
},
|
||||
})
|
||||
}
|
||||
className={sidebarButtonClasses}
|
||||
iconStart={{
|
||||
icon: 'plus',
|
||||
bgClassName: '!bg-transparent rounded-sm',
|
||||
}}
|
||||
data-testid="home-new-file"
|
||||
>
|
||||
Create project
|
||||
</ActionButton>
|
||||
</li>
|
||||
<li className="contents">
|
||||
<ActionButton
|
||||
Element="button"
|
||||
onClick={() =>
|
||||
commandBarActor.send({
|
||||
type: 'Find and select command',
|
||||
data: {
|
||||
groupId: 'application',
|
||||
name: 'Text-to-CAD',
|
||||
argDefaultValues: {
|
||||
method: 'newProject',
|
||||
newProjectName:
|
||||
settings.projects.defaultProjectName.current,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
className={sidebarButtonClasses}
|
||||
iconStart={{
|
||||
icon: 'sparkles',
|
||||
bgClassName: '!bg-transparent rounded-sm',
|
||||
}}
|
||||
data-testid="home-text-to-cad"
|
||||
>
|
||||
Generate with Text-to-CAD
|
||||
</ActionButton>
|
||||
</li>
|
||||
</ul>
|
||||
<ul className="flex flex-col">
|
||||
<li className="contents">
|
||||
<ActionButton
|
||||
Element="externalLink"
|
||||
to="https://zoo.dev/docs"
|
||||
onClick={openExternalBrowserIfDesktop(
|
||||
'https://zoo.dev/account'
|
||||
)}
|
||||
className={sidebarButtonClasses}
|
||||
iconStart={{
|
||||
icon: 'person',
|
||||
bgClassName: '!bg-transparent rounded-sm',
|
||||
}}
|
||||
data-testid="home-account"
|
||||
>
|
||||
View your account
|
||||
</ActionButton>
|
||||
</li>
|
||||
<li className="contents">
|
||||
<ActionButton
|
||||
Element="externalLink"
|
||||
to="https://zoo.dev/blog"
|
||||
onClick={openExternalBrowserIfDesktop('https://zoo.dev/blog')}
|
||||
className={sidebarButtonClasses}
|
||||
iconStart={{
|
||||
icon: 'sketch',
|
||||
bgClassName: '!bg-transparent rounded-sm',
|
||||
}}
|
||||
data-testid="home-blog"
|
||||
>
|
||||
Read the Zoo blog
|
||||
</ActionButton>
|
||||
</li>
|
||||
</ul>
|
||||
</aside>
|
||||
<ProjectGrid
|
||||
searchResults={searchResults}
|
||||
projects={projects}
|
||||
query={query}
|
||||
sort={sort}
|
||||
className="flex-1 col-start-2 -col-end-1 overflow-y-auto pr-2 pb-24"
|
||||
/>
|
||||
<LowerRightControls />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
interface HomeHeaderProps extends HTMLProps<HTMLDivElement> {
|
||||
setQuery: (query: string) => void
|
||||
sort: string
|
||||
setSearchParams: (params: Record<string, string>) => void
|
||||
settings: ReturnType<typeof useSettings>
|
||||
readWriteProjectDir: ReadWriteProjectState
|
||||
}
|
||||
|
||||
function HomeHeader({
|
||||
setQuery,
|
||||
sort,
|
||||
setSearchParams,
|
||||
settings,
|
||||
readWriteProjectDir,
|
||||
...rest
|
||||
}: HomeHeaderProps) {
|
||||
const isSortByModified = sort?.includes('modified') || !sort || sort === null
|
||||
|
||||
return (
|
||||
<section {...rest}>
|
||||
<div className="flex justify-between items-center select-none">
|
||||
<div className="flex gap-8 items-center">
|
||||
<h1 className="text-3xl font-bold">Projects</h1>
|
||||
</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">
|
||||
<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>
|
||||
)
|
||||
}
|
||||
|
||||
interface ProjectGridProps extends HTMLProps<HTMLDivElement> {
|
||||
searchResults: Project[]
|
||||
projects: Project[]
|
||||
query: string
|
||||
sort: string
|
||||
}
|
||||
|
||||
function ProjectGrid({
|
||||
searchResults,
|
||||
projects,
|
||||
query,
|
||||
sort,
|
||||
...rest
|
||||
}: ProjectGridProps) {
|
||||
const state = useSystemIOState()
|
||||
|
||||
return (
|
||||
<section data-testid="home-section" {...rest}>
|
||||
{state.matches(SystemIOMachineStates.readingFolders) ? (
|
||||
<Loading>Loading your Projects...</Loading>
|
||||
) : (
|
||||
<>
|
||||
{searchResults.length > 0 ? (
|
||||
<ul className="grid w-full md:grid-cols-2 xl: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>
|
||||
)
|
||||
}
|
||||
|
||||
/** Type narrowing function of unknown error to a string */
|
||||
function errorMessage(error: unknown): string {
|
||||
if (error !== undefined && error instanceof Error) {
|
||||
return error.message
|
||||
}
|
||||
if (error && typeof error === 'object') {
|
||||
return JSON.stringify(error)
|
||||
}
|
||||
if (typeof error === 'string') {
|
||||
return error
|
||||
}
|
||||
return 'Unknown error'
|
||||
}
|
||||
|
||||
async function handleRenameProject(
|
||||
e: FormEvent<HTMLFormElement>,
|
||||
project: Project
|
||||
@ -195,161 +473,5 @@ const Home = () => {
|
||||
data: { requestedProjectName: 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',
|
||||
},
|
||||
})
|
||||
}
|
||||
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(SystemIOMachineStates.readingFolders) ? (
|
||||
<Loading className="h-screen">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
|
||||
|