From 3d0217793d0a8989585cfead3fe5c31a95bf244f Mon Sep 17 00:00:00 2001 From: Frank Noirot Date: Tue, 9 Jan 2024 21:57:43 -0500 Subject: [PATCH] Sidebar layout (#92) * Change "dashboard" to "view" so pages can share sidebar layout * Rename nav to sidebar, apply only to `/view/*` * Group layouts without changing URLs: Discovered this https://kit.svelte.dev/docs/advanced-routing#advanced-layouts-breaking-out-of-layouts * Move generations to sidebar * Merge branch 'main' into sidebar-layout * New GenerationList, simple sorted list of links * Some polish on the sidebar Signed-off-by: Frank Noirot * Add icons, start polishing view page * Make error version of view page * Persist generations to localStorage * Change "This Week" to "Past 7 Days" * Invalidate threlte renderer on navigation or resize * Add links to billing and github issues * Fix broken conditional for failed submissions * UX polish * Add mobile styling, sidebar * Make prompt text box auto-resize, other polish * preserve query params through homepage redirect * UX polish, colors and submit on enter for textarea * Polish error styling on view page * Polish dashboard, separate out components * Make some 3D lights move with camera * Add screen read labels to feedback buttons * Polish model color and resizing thank you to the @threlte team for setting me straight here: https://github.com/threlte/threlte/issues/817#event-11423757974 * Tablet polish and copy tweak * Add storage version key, remove @square/svelte-store * Remove logs, misc UX polish * Fix new prompt button dark mode hover * Trim prompt text * Add first integration test * Fix unit tests for time buckets * Try adding GitHub CI action * Switch to only do unit tests in CI for now * Remove Playwright steps * Fixes found after user testing * Make polling work better * Clean up home page responsive styles * @jessfraz feedback * Fix endless refresh after model completion --------- Signed-off-by: Frank Noirot --- .env.development | 1 + .github/workflows/unitTests.yml | 22 + .gitignore | 4 + .nvmrc | 1 + README.md | 10 + package.json | 8 +- playwright.config.ts | 36 +- src/components/AccountMenu.svelte | 103 +- src/components/DownloadButton.svelte | 22 +- src/components/ErrorCard.svelte | 44 + src/components/ExamplePrompts.svelte | 44 + src/components/GenerationList.svelte | 131 +- src/components/GenerationListItem.svelte | 208 +- .../GenerationalListItemSkeleton.svelte | 35 - src/components/Icons/Checkmark.svelte | 8 + src/components/Icons/Close.svelte | 8 + src/components/Icons/Menu.svelte | 8 + src/components/Icons/Plus.svelte | 8 + src/components/Icons/Spinner.svelte | 6 + src/components/ModelFeedback.svelte | 12 +- src/components/ModelPreviewer.svelte | 64 - src/components/ModelViewer.svelte | 82 + src/components/Nav.svelte | 33 - src/components/PromptForm.svelte | 129 + src/components/PromptGuide.svelte | 16 + src/components/Sidebar.svelte | 93 + src/hooks.server.ts | 10 +- src/lib/consts.ts | 29 + src/lib/endpoints.ts | 13 +- src/lib/paths.ts | 10 +- src/lib/stores.ts | 67 +- src/lib/time.test.ts | 48 + src/lib/time.ts | 34 + src/routes/(sidebarLayout)/+layout.svelte | 33 + .../(sidebarLayout)/dashboard/+page.svelte | 39 + .../view/[modelId]/+page.svelte | 103 + .../view/[modelId]/+page.ts | 3 +- src/routes/+layout.svelte | 9 +- src/routes/+page.server.ts | 4 +- src/routes/+page.svelte | 88 +- src/routes/dashboard/+page.svelte | 147 - src/routes/view/[modelId]/+page.svelte | 52 - src/styles/app.css | 6 + src/svelteAutosize.d.ts | 5 + tests/e2e.playwright.ts | 11 + tests/test.ts | 6 - yarn-error.log | 4286 +++++++++++++++++ yarn.lock | 264 +- 48 files changed, 5725 insertions(+), 678 deletions(-) create mode 100644 .github/workflows/unitTests.yml create mode 100644 .nvmrc create mode 100644 src/components/ErrorCard.svelte create mode 100644 src/components/ExamplePrompts.svelte delete mode 100644 src/components/GenerationalListItemSkeleton.svelte create mode 100644 src/components/Icons/Checkmark.svelte create mode 100644 src/components/Icons/Close.svelte create mode 100644 src/components/Icons/Menu.svelte create mode 100644 src/components/Icons/Plus.svelte create mode 100644 src/components/Icons/Spinner.svelte delete mode 100644 src/components/ModelPreviewer.svelte create mode 100644 src/components/ModelViewer.svelte delete mode 100644 src/components/Nav.svelte create mode 100644 src/components/PromptForm.svelte create mode 100644 src/components/PromptGuide.svelte create mode 100644 src/components/Sidebar.svelte create mode 100644 src/lib/time.test.ts create mode 100644 src/lib/time.ts create mode 100644 src/routes/(sidebarLayout)/+layout.svelte create mode 100644 src/routes/(sidebarLayout)/dashboard/+page.svelte create mode 100644 src/routes/(sidebarLayout)/view/[modelId]/+page.svelte rename src/routes/{ => (sidebarLayout)}/view/[modelId]/+page.ts (93%) delete mode 100644 src/routes/dashboard/+page.svelte delete mode 100644 src/routes/view/[modelId]/+page.svelte create mode 100644 src/svelteAutosize.d.ts create mode 100644 tests/e2e.playwright.ts delete mode 100644 tests/test.ts create mode 100644 yarn-error.log diff --git a/.env.development b/.env.development index 0ed83a6..74c8d27 100644 --- a/.env.development +++ b/.env.development @@ -1,2 +1,3 @@ VITE_API_BASE_URL=https://api.dev.zoo.dev VITE_SITE_BASE_URL=https://dev.zoo.dev +PLAYWRIGHT_SESSION_COOKIE='' diff --git a/.github/workflows/unitTests.yml b/.github/workflows/unitTests.yml new file mode 100644 index 0000000..4a54e20 --- /dev/null +++ b/.github/workflows/unitTests.yml @@ -0,0 +1,22 @@ +name: Unit Tests +on: + push: + branches: [main] + pull_request: + branches: [main] +jobs: + main: + timeout-minutes: 10 + runs-on: macos-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version-file: '.nvmrc' + cache: 'yarn' + - name: Install dependencies + run: yarn + - name: Run integration tests + run: yarn test:unit run + env: + CI: true diff --git a/.gitignore b/.gitignore index a64e7ae..2839d60 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,7 @@ node_modules !.env.example vite.config.js.timestamp-* vite.config.ts.timestamp-* + + +# playwright artifacts +test-results \ No newline at end of file diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000..1df6fd4 --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +v20.5.0 diff --git a/README.md b/README.md index d364301..ac4cb6f 100644 --- a/README.md +++ b/README.md @@ -42,3 +42,13 @@ CookieDate.setFullYear(CookieDate.getFullYear() + 10) document.cookie = '__Secure-next-auth.session-token=YOUR_TOKEN;Secure;expires=' + CookieDate.toUTCString() + ';' ``` + +### Running Playwright E2E tests locally + +In order to run our Playwright testing suite locally, please set the `PLAYWRIGHT_SESSION_COOKIE` variable within `.env.development` to a token from a logged in local development session. You can retrieve it by: + +1. logging in to the project locally using the method outlined above +2. opening the Application tab in your browser developer tools +3. copying out the value of the cookie titled `__Secure-next-auth.session-token` with the domain of `localhost` + +Now you should be able to run the `yarn test:integration` and `yarn test` commands successfully. diff --git a/package.json b/package.json index 71240c7..ca2b3fc 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "dev": "vite dev", "build": "vite build", "preview": "vite preview", - "test": "npm run test:integration && npm run test:unit", + "test": "npm run test:integration && npm run test:unit run", "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", "lint": "prettier --plugin-search-dir . --check . && eslint .", @@ -20,12 +20,14 @@ "@sveltejs/kit": "^1.20.4", "@testing-library/jest-dom": "^6.2.0", "@testing-library/svelte": "^4.0.5", + "@types/object.groupby": "^1.0.3", "@types/testing-library__jest-dom": "^6.0.0", "@types/three": "^0.157.2", "@typescript-eslint/eslint-plugin": "^6.0.0", "@typescript-eslint/parser": "^6.0.0", "@vitest/ui": "^1.1.2", "autoprefixer": "^10.4.16", + "dotenv": "^16.3.1", "eslint": "^8.28.0", "eslint-config-prettier": "^8.5.0", "eslint-plugin-svelte": "^2.30.0", @@ -49,6 +51,10 @@ "@kittycad/lib": "^0.0.48", "@threlte/core": "^6.1.0", "@threlte/extras": "^7.3.0", + "@types/core-js": "^2.5.8", + "core-js-pure": "^3.35.0", + "object.groupby": "^1.0.1", + "svelte-autosize": "^1.1.0", "three": "^0.157.0" } } diff --git a/playwright.config.ts b/playwright.config.ts index 967c1bc..34d9b41 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -1,12 +1,42 @@ import type { PlaywrightTestConfig } from '@playwright/test' +import { AUTH_COOKIE_NAME } from './src/lib/cookies' +import dotenv from 'dotenv' +import path from 'path' + +dotenv.config({ path: path.resolve(path.dirname('.'), '.env.development') }) +const expiration = new Date() +expiration.setFullYear(expiration.getFullYear() + 1) const config: PlaywrightTestConfig = { + use: { + baseURL: 'https://localhost:3000', + storageState: { + cookies: [ + { + name: AUTH_COOKIE_NAME, + value: process.env.PLAYWRIGHT_SESSION_COOKIE ?? '', + domain: 'localhost', + path: '/', + expires: expiration.getTime() / 1000, + httpOnly: true, + secure: true, + sameSite: 'None' + } + ], + origins: [ + { + origin: 'https://localhost:3000', + localStorage: [] + } + ] + } + }, webServer: { - command: 'npm run build && npm run preview', - port: 4173 + command: 'yarn dev', + port: 3000 }, testDir: 'tests', - testMatch: /(.+\.)?(test|spec)\.[jt]s/ + testMatch: /(.+\.)?(playwright)\.[jt]s/ } export default config diff --git a/src/components/AccountMenu.svelte b/src/components/AccountMenu.svelte index 9d0e727..09706ed 100644 --- a/src/components/AccountMenu.svelte +++ b/src/components/AccountMenu.svelte @@ -2,6 +2,7 @@ import type { Models } from '@kittycad/lib' import { paths } from '$lib/paths' import Person from './Icons/Person.svelte' + import ArrowRight from './Icons/ArrowRight.svelte' export let user: Models['User_type'] let open = false @@ -11,6 +12,14 @@ ((user?.name && user.name.length > 0) || (user?.first_name && user.first_name.length > 0) || (user?.email && user.email.length > 0)) + let displayName = + user?.first_name && user.first_name.length > 0 + ? (user.first_name + (user.last_name ? ' ' + user.last_name : '')).trim() + : user?.name && user.name.length > 0 + ? user.name + : user?.email && user.email.length > 0 + ? user.email + : 'Unnamed User' function dismiss(e: KeyboardEvent) { if (e.key === 'Escape') { @@ -19,43 +28,47 @@ } -
+
+ {displayName}

{user?.first_name - ? user.first_name + (user.last_name ? user.last_name : '') + ? user.first_name + (user.last_name ? ' ' + user.last_name : '') : user?.name || 'Unnamed User'}

@@ -63,11 +76,26 @@

+ Billing Info + + + + Report UI Issue + + + Sign Out
@@ -76,28 +104,28 @@ diff --git a/src/components/DownloadButton.svelte b/src/components/DownloadButton.svelte index 6f0802e..334e57f 100644 --- a/src/components/DownloadButton.svelte +++ b/src/components/DownloadButton.svelte @@ -4,14 +4,21 @@ import type { ConvertResponse } from '../routes/api/convert/[output_format]/+server' import { toKebabCase } from '$lib/toKebabCase' import LoadingIndicator from './LoadingIndicator.svelte' + import { onNavigate } from '$app/navigation' + import { tick } from 'svelte' export let prompt: string = '' export let outputs: PromptResponse['outputs'] export let className: string = '' + let link: HTMLAnchorElement let currentOutput: CADFormat = 'gltf' let outputData = outputs ? outputs[`source.${currentOutput}`] : '' let status: 'loading' | 'ready' | 'failed' = 'ready' + onNavigate(() => { + currentOutput = 'gltf' + }) + $: currentMimeType = CADMIMETypes[currentOutput] $: dataUrl = `data:${currentMimeType};base64,${outputData}` $: fileName = `${toKebabCase(prompt)}.${currentOutput}` @@ -36,20 +43,23 @@ return } - // TODO: handle asynchronous case where the conversion is not yet complete - outputs[`source.${currentOutput}`] = responseData.outputs[`source.${currentOutput}`] outputData = outputs[`source.${currentOutput}`] } status = outputData ? 'ready' : 'failed' + + if (outputData) { + await tick() + link.click() + } }
{#if status == 'ready'} - Download + Download {:else if status == 'loading'} - + {:else} {/if} @@ -77,8 +87,8 @@ diff --git a/src/components/ExamplePrompts.svelte b/src/components/ExamplePrompts.svelte new file mode 100644 index 0000000..67ef8b3 --- /dev/null +++ b/src/components/ExamplePrompts.svelte @@ -0,0 +1,44 @@ + + +
+

+ Example prompts: +

+
+ {#each examplePrompts as prompt (prompt)} + + {/each} +
+
+ + diff --git a/src/components/GenerationList.svelte b/src/components/GenerationList.svelte index d81bcab..99de7e5 100644 --- a/src/components/GenerationList.svelte +++ b/src/components/GenerationList.svelte @@ -1,36 +1,25 @@ -
-

- Your generations -

- {#if $generations.length > 0} -
    - {#each combinedGenerations as item, i} -
  • - - RENDER_THRESHOLD} - /> -
  • - {/each} -
- {/if} - {#if isFetching} - {#each Array(PAGE_SIZE) as i} -
- +
+ {#if Object.keys($generations).length > 0} + {#each Object.entries($generations).toSorted(sortTimeBuckets) as [category, items]} +
+

{category}

+
    + {#each items as item} +
  • + +
  • + {/each} +
{/each} {/if} + {#if isFetching} +

0 ? ' pt-8 border-t' : '')} + > + Fetching your creations + +

+ {:else if Object.keys($generations).length === 0} +

You'll see your creations here once you submit your first prompt

+ {/if} {#if error}

{error}

{/if} - {#if $nextPageToken === null} -

You're all caught up! 🎉

- {/if}
- diff --git a/src/components/GenerationListItem.svelte b/src/components/GenerationListItem.svelte index e84a402..6012e81 100644 --- a/src/components/GenerationListItem.svelte +++ b/src/components/GenerationListItem.svelte @@ -1,171 +1,109 @@ -
- +