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) {
|
if (!this.firstUrl) {
|
||||||
await this.page.getByText('Your Projects').count()
|
await this.page.getByRole('heading', { name: 'Projects' }).count()
|
||||||
this.firstUrl = this.page.url()
|
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 () => {
|
projectsLoaded = async () => {
|
||||||
await expect(this.projectSection).not.toHaveText('Loading your Projects...')
|
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 test.step('Navigate into project', async () => {
|
||||||
await expect(
|
await expect(page.getByRole('heading', { name: 'Projects' })).toBeVisible()
|
||||||
page.getByRole('heading', { name: 'Your Projects' })
|
|
||||||
).toBeVisible()
|
|
||||||
await expect(projectCard).toBeVisible()
|
await expect(projectCard).toBeVisible()
|
||||||
await projectCard.click()
|
await projectCard.click()
|
||||||
await u.waitForPageLoad()
|
await u.waitForPageLoad()
|
||||||
|
@ -723,7 +723,7 @@ test(
|
|||||||
test(
|
test(
|
||||||
'pressing "delete" on home screen should do nothing',
|
'pressing "delete" on home screen should do nothing',
|
||||||
{ tag: '@electron' },
|
{ tag: '@electron' },
|
||||||
async ({ context, page }, testInfo) => {
|
async ({ context, page, homePage }, testInfo) => {
|
||||||
await context.folderSetupFn(async (dir) => {
|
await context.folderSetupFn(async (dir) => {
|
||||||
await fsp.mkdir(`${dir}/router-template-slate`, { recursive: true })
|
await fsp.mkdir(`${dir}/router-template-slate`, { recursive: true })
|
||||||
await fsp.copyFile(
|
await fsp.copyFile(
|
||||||
@ -737,7 +737,7 @@ test(
|
|||||||
|
|
||||||
await expect(page.getByText('router-template-slate')).toBeVisible()
|
await expect(page.getByText('router-template-slate')).toBeVisible()
|
||||||
await expect(page.getByText('Loading your Projects...')).not.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.keyboard.press('Delete')
|
||||||
await page.waitForTimeout(200)
|
await page.waitForTimeout(200)
|
||||||
@ -745,7 +745,7 @@ test(
|
|||||||
|
|
||||||
// expect to still be on the home page
|
// expect to still be on the home page
|
||||||
await expect(page.getByText('router-template-slate')).toBeVisible()
|
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
|
// Locators
|
||||||
const projectsHeading = page.getByRole('heading', {
|
const projectsHeading = page.getByRole('heading', {
|
||||||
name: 'Your projects',
|
name: 'Projects',
|
||||||
})
|
})
|
||||||
const projectLink = page.getByRole('link', { name: 'bracket' })
|
const projectLink = page.getByRole('link', { name: 'bracket' })
|
||||||
const networkHealthIndicator = page.getByTestId('network-toggle')
|
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) {
|
async function goToHomePageFromModeling(page: Page) {
|
||||||
await page.getByTestId('app-logo').click()
|
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 {
|
export function executorInputPath(fileName: string): string {
|
||||||
|
@ -786,7 +786,7 @@ test.describe('Mocked Text-to-CAD API tests', { tag: ['@skipWin'] }, () => {
|
|||||||
test(
|
test(
|
||||||
'Home Page -> Text To CAD -> Existing Project -> Stay in home page -> Reject -> should delete single file',
|
'Home Page -> Text To CAD -> Existing Project -> Stay in home page -> Reject -> should delete single file',
|
||||||
{ tag: '@electron' },
|
{ tag: '@electron' },
|
||||||
async ({ context, page }, testInfo) => {
|
async ({ homePage, page }, testInfo) => {
|
||||||
const projectName = 'my-project-name'
|
const projectName = 'my-project-name'
|
||||||
const prompt = '2x2x2 cube'
|
const prompt = '2x2x2 cube'
|
||||||
await mockPageTextToCAD(page)
|
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
|
// Create and navigate to the project then come home
|
||||||
await createProject({ name: projectName, page, returnHome: true })
|
await createProject({ name: projectName, page, returnHome: true })
|
||||||
|
|
||||||
await expect(page.getByText('Your Projects')).toBeVisible()
|
await homePage.expectIsCurrentPage()
|
||||||
|
|
||||||
await expect(page.getByText('1 file')).toBeVisible()
|
await expect(page.getByText('1 file')).toBeVisible()
|
||||||
|
|
||||||
@ -829,7 +829,7 @@ test.describe('Mocked Text-to-CAD API tests', { tag: ['@skipWin'] }, () => {
|
|||||||
test(
|
test(
|
||||||
'Home Page -> Text To CAD -> Existing Project -> Stay in home page -> Accept -> should navigate to file',
|
'Home Page -> Text To CAD -> Existing Project -> Stay in home page -> Accept -> should navigate to file',
|
||||||
{ tag: '@electron' },
|
{ tag: '@electron' },
|
||||||
async ({ context, page }, testInfo) => {
|
async ({ homePage, page }, testInfo) => {
|
||||||
const u = await getUtils(page)
|
const u = await getUtils(page)
|
||||||
const projectName = 'my-project-name'
|
const projectName = 'my-project-name'
|
||||||
const prompt = '2x2x2 cube'
|
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
|
// Create and navigate to the project then come home
|
||||||
await createProject({ name: projectName, page, returnHome: true })
|
await createProject({ name: projectName, page, returnHome: true })
|
||||||
|
|
||||||
await expect(page.getByText('Your Projects')).toBeVisible()
|
await homePage.expectIsCurrentPage()
|
||||||
|
|
||||||
// open commands
|
// open commands
|
||||||
await page.getByTestId('command-bar-open-button').click()
|
await page.getByTestId('command-bar-open-button').click()
|
||||||
@ -880,7 +880,7 @@ test.describe('Mocked Text-to-CAD API tests', { tag: ['@skipWin'] }, () => {
|
|||||||
test(
|
test(
|
||||||
'Home Page -> Text To CAD -> New Project -> Navigate to the project -> Reject -> should go to home page',
|
'Home Page -> Text To CAD -> New Project -> Navigate to the project -> Reject -> should go to home page',
|
||||||
{ tag: '@electron' },
|
{ tag: '@electron' },
|
||||||
async ({ context, page }, testInfo) => {
|
async ({ homePage, page }, testInfo) => {
|
||||||
const projectName = 'my-project-name'
|
const projectName = 'my-project-name'
|
||||||
const prompt = '2x2x2 cube'
|
const prompt = '2x2x2 cube'
|
||||||
await mockPageTextToCAD(page)
|
await mockPageTextToCAD(page)
|
||||||
@ -919,16 +919,14 @@ test.describe('Mocked Text-to-CAD API tests', { tag: ['@skipWin'] }, () => {
|
|||||||
await page.getByRole('button', { name: 'Reject' }).click()
|
await page.getByRole('button', { name: 'Reject' }).click()
|
||||||
|
|
||||||
// Make sure we went back home
|
// Make sure we went back home
|
||||||
await expect(
|
await homePage.expectIsCurrentPage()
|
||||||
page.getByText('No Projects found, ready to make your first one?')
|
|
||||||
).toBeVisible()
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
test(
|
test(
|
||||||
'Home Page -> Text To CAD -> New Project -> Navigate to the project -> Accept -> should stay in same file',
|
'Home Page -> Text To CAD -> New Project -> Navigate to the project -> Accept -> should stay in same file',
|
||||||
{ tag: '@electron' },
|
{ tag: '@electron' },
|
||||||
async ({ context, page }, testInfo) => {
|
async ({ homePage, page }, testInfo) => {
|
||||||
const projectName = 'my-project-name'
|
const projectName = 'my-project-name'
|
||||||
const prompt = '2x2x2 cube'
|
const prompt = '2x2x2 cube'
|
||||||
await mockPageTextToCAD(page)
|
await mockPageTextToCAD(page)
|
||||||
@ -971,7 +969,7 @@ test.describe('Mocked Text-to-CAD API tests', { tag: ['@skipWin'] }, () => {
|
|||||||
test(
|
test(
|
||||||
'Home Page -> Text To CAD -> Existing Project -> Navigate to the project -> Reject -> should load main.kcl',
|
'Home Page -> Text To CAD -> Existing Project -> Navigate to the project -> Reject -> should load main.kcl',
|
||||||
{ tag: '@electron' },
|
{ tag: '@electron' },
|
||||||
async ({ context, page }, testInfo) => {
|
async ({ homePage, page }, testInfo) => {
|
||||||
const u = await getUtils(page)
|
const u = await getUtils(page)
|
||||||
const projectName = 'my-project-name'
|
const projectName = 'my-project-name'
|
||||||
const prompt = '2x2x2 cube'
|
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
|
// Create and navigate to the project then come home
|
||||||
await createProject({ name: projectName, page, returnHome: true })
|
await createProject({ name: projectName, page, returnHome: true })
|
||||||
|
|
||||||
await expect(page.getByText('Your Projects')).toBeVisible()
|
await homePage.expectIsCurrentPage()
|
||||||
|
|
||||||
// open commands
|
// open commands
|
||||||
await page.getByTestId('command-bar-open-button').click()
|
await page.getByTestId('command-bar-open-button').click()
|
||||||
@ -1026,7 +1024,7 @@ test.describe('Mocked Text-to-CAD API tests', { tag: ['@skipWin'] }, () => {
|
|||||||
test(
|
test(
|
||||||
'Home Page -> Text To CAD -> Existing Project -> Navigate to the project -> Accept -> should load 2x2x2-cube.kcl',
|
'Home Page -> Text To CAD -> Existing Project -> Navigate to the project -> Accept -> should load 2x2x2-cube.kcl',
|
||||||
{ tag: '@electron' },
|
{ tag: '@electron' },
|
||||||
async ({ context, page }, testInfo) => {
|
async ({ homePage, page }, testInfo) => {
|
||||||
const u = await getUtils(page)
|
const u = await getUtils(page)
|
||||||
const projectName = 'my-project-name'
|
const projectName = 'my-project-name'
|
||||||
const prompt = '2x2x2 cube'
|
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
|
// Create and navigate to the project then come home
|
||||||
await createProject({ name: projectName, page, returnHome: true })
|
await createProject({ name: projectName, page, returnHome: true })
|
||||||
|
|
||||||
await expect(page.getByText('Your Projects')).toBeVisible()
|
await homePage.expectIsCurrentPage()
|
||||||
|
|
||||||
// open commands
|
// open commands
|
||||||
await page.getByTestId('command-bar-open-button').click()
|
await page.getByTestId('command-bar-open-button').click()
|
||||||
@ -1083,7 +1081,7 @@ test.describe('Mocked Text-to-CAD API tests', { tag: ['@skipWin'] }, () => {
|
|||||||
test(
|
test(
|
||||||
'Home Page -> Text To CAD -> New Project -> Navigate to different project -> Reject -> should stay in project',
|
'Home Page -> Text To CAD -> New Project -> Navigate to different project -> Reject -> should stay in project',
|
||||||
{ tag: '@electron' },
|
{ tag: '@electron' },
|
||||||
async ({ context, page, homePage }, testInfo) => {
|
async ({ homePage, page }, testInfo) => {
|
||||||
const u = await getUtils(page)
|
const u = await getUtils(page)
|
||||||
const projectName = 'my-project-name'
|
const projectName = 'my-project-name'
|
||||||
const unrelatedProjectName = 'unrelated-project'
|
const unrelatedProjectName = 'unrelated-project'
|
||||||
@ -1097,7 +1095,7 @@ test.describe('Mocked Text-to-CAD API tests', { tag: ['@skipWin'] }, () => {
|
|||||||
returnHome: true,
|
returnHome: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
await expect(page.getByText('Your Projects')).toBeVisible()
|
await homePage.expectIsCurrentPage()
|
||||||
|
|
||||||
// open commands
|
// open commands
|
||||||
await page.getByTestId('command-bar-open-button').click()
|
await page.getByTestId('command-bar-open-button').click()
|
||||||
@ -1160,7 +1158,7 @@ test.describe('Mocked Text-to-CAD API tests', { tag: ['@skipWin'] }, () => {
|
|||||||
test(
|
test(
|
||||||
'Home Page -> Text To CAD -> New Project -> Navigate to different project -> Accept -> should go to new project',
|
'Home Page -> Text To CAD -> New Project -> Navigate to different project -> Accept -> should go to new project',
|
||||||
{ tag: '@electron' },
|
{ tag: '@electron' },
|
||||||
async ({ context, page, homePage }, testInfo) => {
|
async ({ page, homePage }, testInfo) => {
|
||||||
const u = await getUtils(page)
|
const u = await getUtils(page)
|
||||||
const projectName = 'my-project-name'
|
const projectName = 'my-project-name'
|
||||||
const unrelatedProjectName = 'unrelated-project'
|
const unrelatedProjectName = 'unrelated-project'
|
||||||
@ -1174,7 +1172,7 @@ test.describe('Mocked Text-to-CAD API tests', { tag: ['@skipWin'] }, () => {
|
|||||||
returnHome: true,
|
returnHome: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
await expect(page.getByText('Your Projects')).toBeVisible()
|
await homePage.expectIsCurrentPage()
|
||||||
|
|
||||||
// open commands
|
// open commands
|
||||||
await page.getByTestId('command-bar-open-button').click()
|
await page.getByTestId('command-bar-open-button').click()
|
||||||
@ -1233,7 +1231,7 @@ test.describe('Mocked Text-to-CAD API tests', { tag: ['@skipWin'] }, () => {
|
|||||||
test(
|
test(
|
||||||
'Home Page -> Text To CAD -> Existing Project -> Navigate to different project -> Reject -> should stay in same project',
|
'Home Page -> Text To CAD -> Existing Project -> Navigate to different project -> Reject -> should stay in same project',
|
||||||
{ tag: '@electron' },
|
{ tag: '@electron' },
|
||||||
async ({ context, page, homePage }, testInfo) => {
|
async ({ page, homePage }, testInfo) => {
|
||||||
const u = await getUtils(page)
|
const u = await getUtils(page)
|
||||||
const projectName = 'my-project-name'
|
const projectName = 'my-project-name'
|
||||||
const unrelatedProjectName = 'unrelated-project'
|
const unrelatedProjectName = 'unrelated-project'
|
||||||
@ -1246,10 +1244,10 @@ test.describe('Mocked Text-to-CAD API tests', { tag: ['@skipWin'] }, () => {
|
|||||||
page,
|
page,
|
||||||
returnHome: true,
|
returnHome: true,
|
||||||
})
|
})
|
||||||
await expect(page.getByText('Your Projects')).toBeVisible()
|
await homePage.expectIsCurrentPage()
|
||||||
|
|
||||||
await createProject({ name: projectName, page, returnHome: true })
|
await createProject({ name: projectName, page, returnHome: true })
|
||||||
await expect(page.getByText('Your Projects')).toBeVisible()
|
await homePage.expectIsCurrentPage()
|
||||||
|
|
||||||
// open commands
|
// open commands
|
||||||
await page.getByTestId('command-bar-open-button').click()
|
await page.getByTestId('command-bar-open-button').click()
|
||||||
@ -1308,7 +1306,7 @@ test.describe('Mocked Text-to-CAD API tests', { tag: ['@skipWin'] }, () => {
|
|||||||
test(
|
test(
|
||||||
'Home Page -> Text To CAD -> Existing Project -> Navigate to different project -> Accept -> should navigate to new project',
|
'Home Page -> Text To CAD -> Existing Project -> Navigate to different project -> Accept -> should navigate to new project',
|
||||||
{ tag: '@electron' },
|
{ tag: '@electron' },
|
||||||
async ({ context, page, homePage }, testInfo) => {
|
async ({ page, homePage }, testInfo) => {
|
||||||
const u = await getUtils(page)
|
const u = await getUtils(page)
|
||||||
const projectName = 'my-project-name'
|
const projectName = 'my-project-name'
|
||||||
const unrelatedProjectName = 'unrelated-project'
|
const unrelatedProjectName = 'unrelated-project'
|
||||||
@ -1321,10 +1319,10 @@ test.describe('Mocked Text-to-CAD API tests', { tag: ['@skipWin'] }, () => {
|
|||||||
page,
|
page,
|
||||||
returnHome: true,
|
returnHome: true,
|
||||||
})
|
})
|
||||||
await expect(page.getByText('Your Projects')).toBeVisible()
|
await homePage.expectIsCurrentPage()
|
||||||
|
|
||||||
await createProject({ name: projectName, page, returnHome: true })
|
await createProject({ name: projectName, page, returnHome: true })
|
||||||
await expect(page.getByText('Your Projects')).toBeVisible()
|
await homePage.expectIsCurrentPage()
|
||||||
|
|
||||||
// open commands
|
// open commands
|
||||||
await page.getByTestId('command-bar-open-button').click()
|
await page.getByTestId('command-bar-open-button').click()
|
||||||
|
@ -44,7 +44,7 @@ export type ActionButtonProps =
|
|||||||
| ActionButtonAsElement
|
| ActionButtonAsElement
|
||||||
|
|
||||||
export const ActionButton = forwardRef((props: ActionButtonProps, ref) => {
|
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.iconStart
|
||||||
? props.iconEnd
|
? props.iconEnd
|
||||||
? 'px-0' // No padding if both icons are present
|
? 'px-0' // No padding if both icons are present
|
||||||
|
@ -84,7 +84,7 @@ function ProjectCard({
|
|||||||
return (
|
return (
|
||||||
<li
|
<li
|
||||||
{...props}
|
{...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
|
<Link
|
||||||
data-testid="project-link"
|
data-testid="project-link"
|
||||||
@ -93,7 +93,7 @@ function ProjectCard({
|
|||||||
? `${PATHS.FILE}/${encodeURIComponent(project.default_file)}`
|
? `${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
|
project.readWriteAccess
|
||||||
? 'group-hover:!divide-primary group-hover:!hue-rotate-0'
|
? 'group-hover:!divide-primary group-hover:!hue-rotate-0'
|
||||||
: 'cursor-not-allowed'
|
: 'cursor-not-allowed'
|
||||||
|
@ -286,6 +286,11 @@ code {
|
|||||||
@apply bg-chalkboard-20 dark:bg-chalkboard-90;
|
@apply bg-chalkboard-20 dark:bg-chalkboard-90;
|
||||||
@apply border border-t-0 border-b-2 border-chalkboard-30 dark:border-chalkboard-80;
|
@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 {
|
@layer utilities {
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import type { FormEvent } from 'react'
|
import type { FormEvent, HTMLProps } from 'react'
|
||||||
import { useEffect, useRef } from 'react'
|
import { useEffect, useRef } from 'react'
|
||||||
import { toast } from 'react-hot-toast'
|
import { toast } from 'react-hot-toast'
|
||||||
import { useHotkeys } from 'react-hotkeys-hook'
|
import { useHotkeys } from 'react-hotkeys-hook'
|
||||||
@ -38,11 +38,16 @@ import {
|
|||||||
SystemIOMachineStates,
|
SystemIOMachineStates,
|
||||||
} from '@src/machines/systemIO/utils'
|
} from '@src/machines/systemIO/utils'
|
||||||
import type { WebContentSendPayload } from '@src/menu/channels'
|
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,
|
// 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 = useSystemIOState()
|
|
||||||
const readWriteProjectDir = useCanReadWriteProjectDirectory()
|
const readWriteProjectDir = useCanReadWriteProjectDirectory()
|
||||||
|
|
||||||
// Only create the native file menus on desktop
|
// Only create the native file menus on desktop
|
||||||
@ -117,9 +122,9 @@ const Home = () => {
|
|||||||
} else if (data.menuLabel === 'File.Preferences.Keybindings') {
|
} else if (data.menuLabel === 'File.Preferences.Keybindings') {
|
||||||
navigate(PATHS.HOME + PATHS.SETTINGS_KEYBINDINGS)
|
navigate(PATHS.HOME + PATHS.SETTINGS_KEYBINDINGS)
|
||||||
} else if (data.menuLabel === 'File.Preferences.User default units') {
|
} 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') {
|
} 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') {
|
} else if (data.menuLabel === 'File.Sign out') {
|
||||||
authActor.send({ type: 'Log out' })
|
authActor.send({ type: 'Log out' })
|
||||||
} else if (
|
} else if (
|
||||||
@ -136,7 +141,7 @@ const Home = () => {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
} else if (data.menuLabel === 'File.Preferences.Theme color') {
|
} else if (data.menuLabel === 'File.Preferences.Theme color') {
|
||||||
navigate(PATHS.HOME + PATHS.SETTINGS_USER + '#themeColor')
|
navigate(`${PATHS.HOME}${PATHS.SETTINGS_USER}#themeColor`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
useMenuListener(cb)
|
useMenuListener(cb)
|
||||||
@ -162,60 +167,24 @@ const Home = () => {
|
|||||||
const [searchParams, setSearchParams] = useSearchParams()
|
const [searchParams, setSearchParams] = useSearchParams()
|
||||||
const { searchResults, query, setQuery } = useProjectSearch(projects)
|
const { searchResults, query, setQuery } = useProjectSearch(projects)
|
||||||
const sort = searchParams.get('sort_by') ?? 'modified:desc'
|
const sort = searchParams.get('sort_by') ?? 'modified:desc'
|
||||||
|
const sidebarButtonClasses =
|
||||||
const isSortByModified = sort?.includes('modified') || !sort || sort === null
|
'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'
|
||||||
|
|
||||||
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) {
|
|
||||||
systemIOActor.send({
|
|
||||||
type: SystemIOMachineEvents.renameProject,
|
|
||||||
data: {
|
|
||||||
requestedProjectName: String(newProjectName),
|
|
||||||
projectName: project.name,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleDeleteProject(project: Project) {
|
|
||||||
systemIOActor.send({
|
|
||||||
type: SystemIOMachineEvents.deleteProject,
|
|
||||||
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 (
|
return (
|
||||||
<div className="relative flex flex-col h-screen overflow-hidden" ref={ref}>
|
<div className="relative flex flex-col h-screen overflow-hidden" ref={ref}>
|
||||||
<AppHeader showToolbar={false} />
|
<AppHeader showToolbar={false} />
|
||||||
<div className="w-full flex flex-col overflow-hidden max-w-5xl px-4 mx-auto mt-24 lg:px-2">
|
<div className="overflow-hidden home-layout max-w-4xl xl:max-w-7xl mb-12 px-4 mx-auto mt-24 lg:px-0">
|
||||||
<section>
|
<HomeHeader
|
||||||
<div className="flex justify-between items-center select-none">
|
setQuery={setQuery}
|
||||||
<div className="flex gap-8 items-center">
|
sort={sort}
|
||||||
<h1 className="text-3xl font-bold">Your Projects</h1>
|
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
|
<ActionButton
|
||||||
Element="button"
|
Element="button"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
@ -227,129 +196,282 @@ const Home = () => {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
className="group !bg-primary !text-chalkboard-10 !border-primary hover:shadow-inner hover:hue-rotate-15"
|
className={sidebarButtonClasses}
|
||||||
iconStart={{
|
iconStart={{
|
||||||
icon: 'plus',
|
icon: 'plus',
|
||||||
bgClassName: '!bg-transparent rounded-sm',
|
bgClassName: '!bg-transparent rounded-sm',
|
||||||
iconClassName:
|
|
||||||
'!text-chalkboard-10 transition-transform group-active:rotate-90',
|
|
||||||
}}
|
}}
|
||||||
data-testid="home-new-file"
|
data-testid="home-new-file"
|
||||||
>
|
>
|
||||||
Create project
|
Create project
|
||||||
</ActionButton>
|
</ActionButton>
|
||||||
</div>
|
</li>
|
||||||
<div className="flex gap-2 items-center">
|
<li className="contents">
|
||||||
<ProjectSearchBar setQuery={setQuery} />
|
|
||||||
<small>Sort by</small>
|
|
||||||
<ActionButton
|
<ActionButton
|
||||||
Element="button"
|
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={() =>
|
onClick={() =>
|
||||||
setSearchParams(getNextSearchParams(sort, 'name'))
|
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={{
|
iconStart={{
|
||||||
icon: getSortIcon(sort, 'name'),
|
icon: 'sparkles',
|
||||||
bgClassName: 'bg-transparent',
|
bgClassName: '!bg-transparent rounded-sm',
|
||||||
iconClassName: !sort.includes('name')
|
|
||||||
? '!text-chalkboard-90 dark:!text-chalkboard-30'
|
|
||||||
: '',
|
|
||||||
}}
|
}}
|
||||||
|
data-testid="home-text-to-cad"
|
||||||
>
|
>
|
||||||
Name
|
Generate with Text-to-CAD
|
||||||
</ActionButton>
|
</ActionButton>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<ul className="flex flex-col">
|
||||||
|
<li className="contents">
|
||||||
<ActionButton
|
<ActionButton
|
||||||
Element="button"
|
Element="externalLink"
|
||||||
data-testid="home-sort-by-modified"
|
to="https://zoo.dev/docs"
|
||||||
className={
|
onClick={openExternalBrowserIfDesktop(
|
||||||
'text-xs border-primary/10 ' +
|
'https://zoo.dev/account'
|
||||||
(!isSortByModified
|
)}
|
||||||
? 'text-chalkboard-80 dark:text-chalkboard-40'
|
className={sidebarButtonClasses}
|
||||||
: '')
|
|
||||||
}
|
|
||||||
onClick={() =>
|
|
||||||
setSearchParams(getNextSearchParams(sort, 'modified'))
|
|
||||||
}
|
|
||||||
iconStart={{
|
iconStart={{
|
||||||
icon: sort ? getSortIcon(sort, 'modified') : 'arrowDown',
|
icon: 'person',
|
||||||
bgClassName: 'bg-transparent',
|
bgClassName: '!bg-transparent rounded-sm',
|
||||||
iconClassName: !isSortByModified
|
|
||||||
? '!text-chalkboard-90 dark:!text-chalkboard-30'
|
|
||||||
: '',
|
|
||||||
}}
|
}}
|
||||||
|
data-testid="home-account"
|
||||||
>
|
>
|
||||||
Last Modified
|
View your account
|
||||||
</ActionButton>
|
</ActionButton>
|
||||||
</div>
|
</li>
|
||||||
</div>
|
<li className="contents">
|
||||||
<p className="my-4 text-sm text-chalkboard-80 dark:text-chalkboard-30">
|
<ActionButton
|
||||||
Loaded from{' '}
|
Element="externalLink"
|
||||||
<Link
|
to="https://zoo.dev/blog"
|
||||||
data-testid="project-directory-settings-link"
|
onClick={openExternalBrowserIfDesktop('https://zoo.dev/blog')}
|
||||||
to={`${PATHS.HOME + PATHS.SETTINGS_USER}#projectDirectory`}
|
className={sidebarButtonClasses}
|
||||||
className="text-chalkboard-90 dark:text-chalkboard-20 underline underline-offset-2"
|
iconStart={{
|
||||||
>
|
icon: 'sketch',
|
||||||
{settings.app.projectDirectory.current}
|
bgClassName: '!bg-transparent rounded-sm',
|
||||||
</Link>
|
}}
|
||||||
.
|
data-testid="home-blog"
|
||||||
</p>
|
>
|
||||||
{!readWriteProjectDir.value && (
|
Read the Zoo blog
|
||||||
<section>
|
</ActionButton>
|
||||||
<div className="flex items-center select-none">
|
</li>
|
||||||
<div className="flex gap-8 items-center justify-between grow bg-destroy-80 text-white py-1 px-4 my-2 rounded-sm grow">
|
</ul>
|
||||||
<p className="">{errorMessage(readWriteProjectDir.error)}</p>
|
</aside>
|
||||||
<Link
|
<ProjectGrid
|
||||||
data-testid="project-directory-settings-link"
|
searchResults={searchResults}
|
||||||
to={`${PATHS.HOME + PATHS.SETTINGS_USER}#projectDirectory`}
|
projects={projects}
|
||||||
className="py-1 text-white underline underline-offset-2 text-sm"
|
query={query}
|
||||||
>
|
sort={sort}
|
||||||
Change Project Directory
|
className="flex-1 col-start-2 -col-end-1 overflow-y-auto pr-2 pb-24"
|
||||||
</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 />
|
<LowerRightControls />
|
||||||
</div>
|
</div>
|
||||||
</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
|
||||||
|
) {
|
||||||
|
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) {
|
||||||
|
systemIOActor.send({
|
||||||
|
type: SystemIOMachineEvents.renameProject,
|
||||||
|
data: {
|
||||||
|
requestedProjectName: String(newProjectName),
|
||||||
|
projectName: project.name,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleDeleteProject(project: Project) {
|
||||||
|
systemIOActor.send({
|
||||||
|
type: SystemIOMachineEvents.deleteProject,
|
||||||
|
data: { requestedProjectName: project.name },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
export default Home
|
export default Home
|
||||||
|