Modeling view appearance final tweaks (#6425)

* Remove bug button from LowerRightControls

There is a "report a bug" button in the help menus, both native and
lower-right corner. This is overkill, and users are not using coredump
well.

* Remove coredump from refresh UI button

* Add a "Refresh app" command to palette

* Update snapshots

* Rework "Refresh and report bug" menu item to "Report a bug"

* Add refresh button to sidebar

* Convert upper-right refresh button to Share

* Tweak styles of command button

* Make anonymous user icon same size as known user image

* Remove ModelStateIndicator

* Use hotkeyDisplay for the sidebar too

* Update snapshots

* Remove tooltip from command bar open button

* tsc, lint, and fmt
This commit is contained in:
Frank Noirot
2025-04-23 15:20:45 -04:00
committed by GitHub
parent 45e17c50e7
commit f03a684eec
44 changed files with 156 additions and 291 deletions

View File

@ -409,11 +409,7 @@ test.describe('Native file menu', { tag: ['@electron'] }, () => {
) )
.toBe(true) .toBe(true)
}) })
test('Home.Help.Refresh and report a bug', async ({ test('Home.Help.Report a bug', async ({ tronApp, cmdBar, page }) => {
tronApp,
cmdBar,
page,
}) => {
if (!tronApp) fail() if (!tronApp) fail()
// Run electron snippet to find the Menu! // Run electron snippet to find the Menu!
await page.waitForTimeout(100) // wait for createModelingPageMenu() to run await page.waitForTimeout(100) // wait for createModelingPageMenu() to run
@ -424,9 +420,8 @@ test.describe('Native file menu', { tag: ['@electron'] }, () => {
if (!app || !app.applicationMenu) { if (!app || !app.applicationMenu) {
return false return false
} }
const menu = app.applicationMenu.getMenuItemById( const menu =
'Help.Refresh and report a bug' app.applicationMenu.getMenuItemById('Help.Report a bug')
)
if (!menu) return false if (!menu) return false
menu.click() menu.click()
return true return true
@ -2291,7 +2286,7 @@ test.describe('Native file menu', { tag: ['@electron'] }, () => {
if (!menu) fail() if (!menu) fail()
}) })
}) })
test('Modeling.Help.Refresh and report a bug', async ({ test('Modeling.Help.Report a bug', async ({
tronApp, tronApp,
cmdBar, cmdBar,
page, page,
@ -2315,9 +2310,8 @@ test.describe('Native file menu', { tag: ['@electron'] }, () => {
async () => async () =>
await tronApp.electron.evaluate(async ({ app }) => { await tronApp.electron.evaluate(async ({ app }) => {
if (!app || !app.applicationMenu) return false if (!app || !app.applicationMenu) return false
const menu = app.applicationMenu.getMenuItemById( const menu =
'Help.Refresh and report a bug' app.applicationMenu.getMenuItemById('Help.Report a bug')
)
if (!menu) return false if (!menu) return false
menu.click() menu.click()
return true return true

View File

@ -400,11 +400,6 @@ test(
await expect(page.getByText('broken-code')).toBeVisible() await expect(page.getByText('broken-code')).toBeVisible()
await page.getByText('broken-code').click() await page.getByText('broken-code').click()
// Gotcha: You can not use scene.settled() since the KCL code is going to fail
await expect(
page.getByTestId('model-state-indicator-playing')
).toBeAttached()
// Gotcha: Scroll to the text content in code mirror because CodeMirror lazy loads DOM content // Gotcha: Scroll to the text content in code mirror because CodeMirror lazy loads DOM content
await editor.scrollToText( await editor.scrollToText(
"|> line(end = [0, wallMountL], tag = 'outerEdge')" "|> line(end = [0, wallMountL], tag = 'outerEdge')"
@ -779,7 +774,9 @@ test.describe(`Project management commands`, () => {
// Constants and locators // Constants and locators
const projectHomeLink = page.getByTestId('project-link') const projectHomeLink = page.getByTestId('project-link')
const commandButton = page.getByRole('button', { name: 'Commands' }) const commandButton = page.getByRole('button', { name: 'Commands' })
const commandOption = page.getByRole('option', { name: 'rename project' }) const commandOption = page.getByRole('option', {
name: 'rename project',
})
const projectNameOption = page.getByRole('option', { name: projectName }) const projectNameOption = page.getByRole('option', { name: projectName })
const projectRenamedName = `untitled` const projectRenamedName = `untitled`
// const projectMenuButton = page.getByTestId('project-sidebar-toggle') // const projectMenuButton = page.getByTestId('project-sidebar-toggle')
@ -839,7 +836,9 @@ test.describe(`Project management commands`, () => {
// Constants and locators // Constants and locators
const projectHomeLink = page.getByTestId('project-link') const projectHomeLink = page.getByTestId('project-link')
const commandButton = page.getByRole('button', { name: 'Commands' }) const commandButton = page.getByRole('button', { name: 'Commands' })
const commandOption = page.getByRole('option', { name: 'delete project' }) const commandOption = page.getByRole('option', {
name: 'delete project',
})
const projectNameOption = page.getByRole('option', { name: projectName }) const projectNameOption = page.getByRole('option', { name: projectName })
const commandWarning = page.getByText('Are you sure you want to delete?') const commandWarning = page.getByText('Are you sure you want to delete?')
const commandSubmitButton = page.getByRole('button', { const commandSubmitButton = page.getByRole('button', {
@ -891,7 +890,9 @@ test.describe(`Project management commands`, () => {
// Constants and locators // Constants and locators
const projectHomeLink = page.getByTestId('project-link') const projectHomeLink = page.getByTestId('project-link')
const commandButton = page.getByRole('button', { name: 'Commands' }) const commandButton = page.getByRole('button', { name: 'Commands' })
const commandOption = page.getByRole('option', { name: 'rename project' }) const commandOption = page.getByRole('option', {
name: 'rename project',
})
const projectNameOption = page.getByRole('option', { name: projectName }) const projectNameOption = page.getByRole('option', { name: projectName })
const projectRenamedName = `untitled` const projectRenamedName = `untitled`
const commandContinueButton = page.getByRole('button', { const commandContinueButton = page.getByRole('button', {
@ -947,7 +948,9 @@ test.describe(`Project management commands`, () => {
// Constants and locators // Constants and locators
const projectHomeLink = page.getByTestId('project-link') const projectHomeLink = page.getByTestId('project-link')
const commandButton = page.getByRole('button', { name: 'Commands' }) const commandButton = page.getByRole('button', { name: 'Commands' })
const commandOption = page.getByRole('option', { name: 'delete project' }) const commandOption = page.getByRole('option', {
name: 'delete project',
})
const projectNameOption = page.getByRole('option', { name: projectName }) const projectNameOption = page.getByRole('option', { name: projectName })
const commandWarning = page.getByText('Are you sure you want to delete?') const commandWarning = page.getByText('Are you sure you want to delete?')
const commandSubmitButton = page.getByRole('button', { const commandSubmitButton = page.getByRole('button', {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 57 KiB

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 57 KiB

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 KiB

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 75 KiB

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 59 KiB

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 134 KiB

After

Width:  |  Height:  |  Size: 134 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 117 KiB

After

Width:  |  Height:  |  Size: 117 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 69 KiB

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 65 KiB

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 67 KiB

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 KiB

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

After

Width:  |  Height:  |  Size: 64 KiB

View File

@ -36,7 +36,6 @@ export const headerMasks = (page: Page) => [
] ]
export const networkingMasks = (page: Page) => [ export const networkingMasks = (page: Page) => [
page.getByTestId('model-state-indicator'),
page.getByTestId('network-toggle'), page.getByTestId('network-toggle'),
] ]
@ -85,12 +84,6 @@ async function waitForPageLoadWithRetry(page: Page) {
await expect(async () => { await expect(async () => {
await page.goto('/') await page.goto('/')
const errorMessage = 'App failed to load - 🔃 Retrying ...' const errorMessage = 'App failed to load - 🔃 Retrying ...'
await expect(
page.getByTestId('model-state-indicator-playing'),
errorMessage
).toBeAttached({
timeout: 20_000,
})
await expect( await expect(
page.getByRole('button', { name: 'sketch Start Sketch' }), page.getByRole('button', { name: 'sketch Start Sketch' }),
@ -103,11 +96,6 @@ async function waitForPageLoadWithRetry(page: Page) {
// lee: This needs to be replaced by scene.settled() eventually. // lee: This needs to be replaced by scene.settled() eventually.
async function waitForPageLoad(page: Page) { async function waitForPageLoad(page: Page) {
// wait for all spinners to be gone
await expect(page.getByTestId('model-state-indicator-playing')).toBeVisible({
timeout: 20_000,
})
await expect(page.getByRole('button', { name: 'Start Sketch' })).toBeEnabled({ await expect(page.getByRole('button', { name: 'Start Sketch' })).toBeEnabled({
timeout: 20_000, timeout: 20_000,
}) })

View File

@ -1,4 +1,4 @@
import { useEffect, useMemo, 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'
import ModalContainer from 'react-modal-promise' import ModalContainer from 'react-modal-promise'
@ -21,20 +21,14 @@ import { useAbsoluteFilePath } from '@src/hooks/useAbsoluteFilePath'
import { useCreateFileLinkQuery } from '@src/hooks/useCreateFileLinkQueryWatcher' import { useCreateFileLinkQuery } from '@src/hooks/useCreateFileLinkQueryWatcher'
import { useEngineConnectionSubscriptions } from '@src/hooks/useEngineConnectionSubscriptions' import { useEngineConnectionSubscriptions } from '@src/hooks/useEngineConnectionSubscriptions'
import { useHotKeyListener } from '@src/hooks/useHotKeyListener' import { useHotKeyListener } from '@src/hooks/useHotKeyListener'
import { CoreDumpManager } from '@src/lib/coredump'
import { writeProjectThumbnailFile } from '@src/lib/desktop' import { writeProjectThumbnailFile } from '@src/lib/desktop'
import useHotkeyWrapper from '@src/lib/hotkeyWrapper' import useHotkeyWrapper from '@src/lib/hotkeyWrapper'
import { isDesktop } from '@src/lib/isDesktop' import { isDesktop } from '@src/lib/isDesktop'
import { PATHS } from '@src/lib/paths' import { PATHS } from '@src/lib/paths'
import { takeScreenshotOfVideoStreamCanvas } from '@src/lib/screenshot' import { takeScreenshotOfVideoStreamCanvas } from '@src/lib/screenshot'
import { import { sceneInfra } from '@src/lib/singletons'
codeManager,
engineCommandManager,
rustContext,
sceneInfra,
} from '@src/lib/singletons'
import { maybeWriteToDisk } from '@src/lib/telemetry' import { maybeWriteToDisk } from '@src/lib/telemetry'
import { type IndexLoaderData } from '@src/lib/types' import type { IndexLoaderData } from '@src/lib/types'
import { import {
engineStreamActor, engineStreamActor,
useSettings, useSettings,
@ -43,6 +37,8 @@ import {
import { commandBarActor } from '@src/machines/commandBarMachine' import { commandBarActor } from '@src/machines/commandBarMachine'
import { EngineStreamTransition } from '@src/machines/engineStreamMachine' import { EngineStreamTransition } from '@src/machines/engineStreamMachine'
import { onboardingPaths } from '@src/routes/Onboarding/paths' import { onboardingPaths } from '@src/routes/Onboarding/paths'
import { CommandBarOpenButton } from '@src/components/CommandBarOpenButton'
import { ShareButton } from '@src/components/ShareButton'
// CYCLIC REF // CYCLIC REF
sceneInfra.camControls.engineStreamActor = engineStreamActor sceneInfra.camControls.engineStreamActor = engineStreamActor
@ -93,17 +89,6 @@ export function App() {
const settings = useSettings() const settings = useSettings()
const authToken = useToken() const authToken = useToken()
const coreDumpManager = useMemo(
() =>
new CoreDumpManager(
engineCommandManager,
codeManager,
rustContext,
authToken
),
[]
)
const { const {
app: { onboardingStatus }, app: { onboardingStatus },
} = settings } = settings
@ -163,15 +148,18 @@ export function App() {
return ( return (
<div className="relative h-full flex flex-col" ref={ref}> <div className="relative h-full flex flex-col" ref={ref}>
<AppHeader <AppHeader
className={'transition-opacity transition-duration-75 ' + paneOpacity} className={`transition-opacity transition-duration-75 ${paneOpacity}`}
project={{ project, file }} project={{ project, file }}
enableMenu={true} enableMenu={true}
/> >
<CommandBarOpenButton />
<ShareButton />
</AppHeader>
<ModalContainer /> <ModalContainer />
<ModelingSidebar paneOpacity={paneOpacity} /> <ModelingSidebar paneOpacity={paneOpacity} />
<EngineStream pool={pool} authToken={authToken} /> <EngineStream pool={pool} authToken={authToken} />
{/* <CamToggle /> */} {/* <CamToggle /> */}
<LowerRightControls coreDumpManager={coreDumpManager}> <LowerRightControls>
<UnitsMenu /> <UnitsMenu />
<Gizmo /> <Gizmo />
</LowerRightControls> </LowerRightControls>

View File

@ -1,7 +1,6 @@
import { Toolbar } from '@src/Toolbar' import { Toolbar } from '@src/Toolbar'
import { CommandBarOpenButton } from '@src/components/CommandBarOpenButton' import { CommandBarOpenButton } from '@src/components/CommandBarOpenButton'
import ProjectSidebarMenu from '@src/components/ProjectSidebarMenu' import ProjectSidebarMenu from '@src/components/ProjectSidebarMenu'
import { RefreshButton } from '@src/components/RefreshButton'
import UserSidebarMenu from '@src/components/UserSidebarMenu' import UserSidebarMenu from '@src/components/UserSidebarMenu'
import { isDesktop } from '@src/lib/isDesktop' import { isDesktop } from '@src/lib/isDesktop'
import { type IndexLoaderData } from '@src/lib/types' import { type IndexLoaderData } from '@src/lib/types'
@ -49,14 +48,9 @@ export const AppHeader = ({
<div className="flex-grow flex justify-center max-w-lg md:max-w-xl lg:max-w-2xl xl:max-w-4xl 2xl:max-w-5xl"> <div className="flex-grow flex justify-center max-w-lg md:max-w-xl lg:max-w-2xl xl:max-w-4xl 2xl:max-w-5xl">
{showToolbar && <Toolbar />} {showToolbar && <Toolbar />}
</div> </div>
<div className="flex items-center gap-1 py-1 ml-auto"> <div className="flex items-center gap-2 py-1 ml-auto">
{/* If there are children, show them, otherwise show User menu */} {/* If there are children, show them, otherwise show User menu */}
{children || ( {children || <CommandBarOpenButton />}
<>
<CommandBarOpenButton />
<RefreshButton />
</>
)}
<UserSidebarMenu user={user} /> <UserSidebarMenu user={user} />
</div> </div>
</header> </header>

View File

@ -2,18 +2,21 @@ import { COMMAND_PALETTE_HOTKEY } from '@src/components/CommandBar/CommandBar'
import usePlatform from '@src/hooks/usePlatform' import usePlatform from '@src/hooks/usePlatform'
import { hotkeyDisplay } from '@src/lib/hotkeyWrapper' import { hotkeyDisplay } from '@src/lib/hotkeyWrapper'
import { commandBarActor } from '@src/machines/commandBarMachine' import { commandBarActor } from '@src/machines/commandBarMachine'
import { CustomIcon } from '@src/components/CustomIcon'
export function CommandBarOpenButton() { export function CommandBarOpenButton() {
const platform = usePlatform() const platform = usePlatform()
return ( return (
<button <button
className="group rounded-full flex items-center justify-center gap-2 px-2 py-1 bg-primary/10 dark:bg-chalkboard-90 dark:backdrop-blur-sm border-primary hover:border-primary dark:border-chalkboard-50 dark:hover:border-inherit text-primary dark:text-inherit" type="button"
className="flex gap-1 items-center py-0 px-0.5 m-0 text-primary dark:text-inherit bg-chalkboard-10/80 dark:bg-chalkboard-100/50 hover:bg-chalkboard-10 dark:hover:bg-chalkboard-100 border border-solid border-primary/50 hover:border-primary active:border-primary"
onClick={() => commandBarActor.send({ type: 'Open' })} onClick={() => commandBarActor.send({ type: 'Open' })}
data-testid="command-bar-open-button" data-testid="command-bar-open-button"
> >
<CustomIcon name="command" className="w-5 h-5" />
<span>Commands</span> <span>Commands</span>
<kbd className="bg-primary/10 dark:bg-chalkboard-80 dark:group-hover:bg-primary font-mono rounded-sm dark:text-inherit inline-block px-1 border-primary dark:border-chalkboard-90"> <kbd className="dark:bg-chalkboard-80 font-mono rounded-sm text-primary/70 dark:text-inherit inline-block px-1">
{hotkeyDisplay(COMMAND_PALETTE_HOTKEY, platform)} {hotkeyDisplay(COMMAND_PALETTE_HOTKEY, platform)}
</kbd> </kbd>
</button> </button>

View File

@ -311,6 +311,22 @@ const CustomIconMap = {
/> />
</svg> </svg>
), ),
command: (
<svg
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M4.70711 6L8.20711 9.5L8.56066 9.85355L8.20711 10.2071L4.70711 13.7071L4 13L7.14645 9.85355L4 6.70711L4.70711 6ZM15.3536 11.3536H9.35356V12.3536H15.3536V11.3536Z"
fill="currentColor"
/>
</svg>
),
dimension: ( dimension: (
<svg <svg
viewBox="0 0 20 20" viewBox="0 0 20 20"

View File

@ -1,26 +1,19 @@
import toast from 'react-hot-toast'
import { Link, useLocation } from 'react-router-dom' import { Link, useLocation } from 'react-router-dom'
import { CustomIcon } from '@src/components/CustomIcon' import { CustomIcon } from '@src/components/CustomIcon'
import { HelpMenu } from '@src/components/HelpMenu' import { HelpMenu } from '@src/components/HelpMenu'
import { ModelStateIndicator } from '@src/components/ModelStateIndicator'
import { NetworkHealthIndicator } from '@src/components/NetworkHealthIndicator' import { NetworkHealthIndicator } from '@src/components/NetworkHealthIndicator'
import { NetworkMachineIndicator } from '@src/components/NetworkMachineIndicator' import { NetworkMachineIndicator } from '@src/components/NetworkMachineIndicator'
import Tooltip from '@src/components/Tooltip' import Tooltip from '@src/components/Tooltip'
import { useAbsoluteFilePath } from '@src/hooks/useAbsoluteFilePath' import { useAbsoluteFilePath } from '@src/hooks/useAbsoluteFilePath'
import { coreDump } from '@src/lang/wasm' import { openExternalBrowserIfDesktop } from '@src/lib/openWindow'
import type { CoreDumpManager } from '@src/lib/coredump'
import openWindow, { openExternalBrowserIfDesktop } from '@src/lib/openWindow'
import { PATHS } from '@src/lib/paths' import { PATHS } from '@src/lib/paths'
import { reportRejection } from '@src/lib/trap'
import { APP_VERSION, getReleaseUrl } from '@src/routes/utils' import { APP_VERSION, getReleaseUrl } from '@src/routes/utils'
export function LowerRightControls({ export function LowerRightControls({
children, children,
coreDumpManager,
}: { }: {
children?: React.ReactNode children?: React.ReactNode
coreDumpManager?: CoreDumpManager
}) { }) {
const location = useLocation() const location = useLocation()
const filePath = useAbsoluteFilePath() const filePath = useAbsoluteFilePath()
@ -28,50 +21,10 @@ export function LowerRightControls({
const linkOverrideClassName = const linkOverrideClassName =
'!text-chalkboard-70 hover:!text-chalkboard-80 dark:!text-chalkboard-40 dark:hover:!text-chalkboard-30' '!text-chalkboard-70 hover:!text-chalkboard-80 dark:!text-chalkboard-40 dark:hover:!text-chalkboard-30'
function reportbug(event: {
preventDefault: () => void
stopPropagation: () => void
}) {
event?.preventDefault()
event?.stopPropagation()
if (!coreDumpManager) {
// open default reporting option
openWindow(
'https://github.com/KittyCAD/modeling-app/issues/new/choose'
).catch(reportRejection)
} else {
toast
.promise(
coreDump(coreDumpManager, true),
{
loading: 'Preparing bug report...',
success: 'Bug report opened in new window',
error: 'Unable to export a core dump. Using default reporting.',
},
{
success: {
// Note: this extended duration is especially important for Playwright e2e testing
// default duration is 2000 - https://react-hot-toast.com/docs/toast#default-durations
duration: 6000,
},
}
)
.catch((err: Error) => {
if (err) {
openWindow(
'https://github.com/KittyCAD/modeling-app/issues/new/choose'
).catch(reportRejection)
}
})
}
}
return ( return (
<section className="fixed bottom-2 right-2 flex flex-col items-end gap-3 pointer-events-none"> <section className="fixed bottom-2 right-2 flex flex-col items-end gap-3 pointer-events-none">
{children} {children}
<menu className="flex items-center justify-end gap-3 pointer-events-auto"> <menu className="flex items-center justify-end gap-3 pointer-events-auto">
{!location.pathname.startsWith(PATHS.HOME) && <ModelStateIndicator />}
<a <a
onClick={openExternalBrowserIfDesktop(getReleaseUrl())} onClick={openExternalBrowserIfDesktop(getReleaseUrl())}
href={getReleaseUrl()} href={getReleaseUrl()}
@ -81,20 +34,6 @@ export function LowerRightControls({
> >
v{APP_VERSION} v{APP_VERSION}
</a> </a>
<a
onClick={reportbug}
href="https://github.com/KittyCAD/modeling-app/issues/new/choose"
target="_blank"
rel="noopener noreferrer"
>
<CustomIcon
name="bug"
className={`w-5 h-5 ${linkOverrideClassName}`}
/>
<Tooltip position="top" contentClassName="text-xs">
Report a bug
</Tooltip>
</a>
<Link <Link
to={ to={
location.pathname.includes(PATHS.FILE) location.pathname.includes(PATHS.FILE)

View File

@ -1,52 +0,0 @@
import { engineStreamActor } from '@src/machines/appMachine'
import { EngineStreamState } from '@src/machines/engineStreamMachine'
import { useSelector } from '@xstate/react'
import { faPause, faPlay, faSpinner } from '@fortawesome/free-solid-svg-icons'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
export const ModelStateIndicator = () => {
const engineStreamState = useSelector(engineStreamActor, (state) => state)
let className = 'w-6 h-6 '
let icon = <div className={className}></div>
let dataTestId = 'model-state-indicator'
if (engineStreamState.value === EngineStreamState.Paused) {
className += 'text-secondary'
icon = (
<FontAwesomeIcon
data-testid={dataTestId + '-paused'}
icon={faPause}
width="20"
height="20"
/>
)
} else if (engineStreamState.value === EngineStreamState.Playing) {
className += 'text-secondary'
icon = (
<FontAwesomeIcon
data-testid={dataTestId + '-playing'}
icon={faPlay}
width="20"
height="20"
/>
)
} else {
className += 'text-secondary'
icon = (
<FontAwesomeIcon
data-testid={dataTestId + '-resuming'}
icon={faSpinner}
width="20"
height="20"
/>
)
}
return (
<div className={className} data-testid="model-state-indicator">
{icon}
</div>
)
}

View File

@ -25,6 +25,10 @@ import { isDesktop } from '@src/lib/isDesktop'
import { useSettings } from '@src/machines/appMachine' import { useSettings } from '@src/machines/appMachine'
import { commandBarActor } from '@src/machines/commandBarMachine' import { commandBarActor } from '@src/machines/commandBarMachine'
import { onboardingPaths } from '@src/routes/Onboarding/paths' import { onboardingPaths } from '@src/routes/Onboarding/paths'
import { reportRejection } from '@src/lib/trap'
import { refreshPage } from '@src/lib/utils'
import { hotkeyDisplay } from '@src/lib/hotkeyWrapper'
import usePlatform from '@src/hooks/usePlatform'
interface ModelingSidebarProps { interface ModelingSidebarProps {
paneOpacity: '' | 'opacity-20' | 'opacity-40' paneOpacity: '' | 'opacity-20' | 'opacity-40'
@ -86,18 +90,6 @@ export function ModelingSidebar({ paneOpacity }: ModelingSidebarProps) {
data: { name: 'load-external-model', groupId: 'code' }, data: { name: 'load-external-model', groupId: 'code' },
}), }),
}, },
{
id: 'share-link',
title: 'Share part via Zoo link',
sidebarName: 'Share part via Zoo link',
icon: 'link',
keybinding: 'Mod + Alt + S',
action: () =>
commandBarActor.send({
type: 'Find and select command',
data: { name: 'share-file-link', groupId: 'code' },
}),
},
{ {
id: 'export', id: 'export',
title: 'Export part', title: 'Export part',
@ -130,6 +122,17 @@ export function ModelingSidebar({ paneOpacity }: ModelingSidebarProps) {
return machineManager.noMachinesReason() return machineManager.noMachinesReason()
}, },
}, },
{
id: 'refresh',
title: 'Refresh app',
sidebarName: 'Refresh app',
icon: 'arrowRotateRight',
keybinding: 'Mod + R',
// eslint-disable-next-line @typescript-eslint/no-misused-promises
action: async () => {
refreshPage('Sidebar button').catch(reportRejection)
},
},
] ]
const filteredActions: SidebarAction[] = sidebarActions.filter( const filteredActions: SidebarAction[] = sidebarActions.filter(
(action) => (action) =>
@ -340,6 +343,7 @@ function ModelingPaneButton({
disabledText, disabledText,
...props ...props
}: ModelingPaneButtonProps) { }: ModelingPaneButtonProps) {
const platform = usePlatform()
useHotkeys(paneConfig.keybinding, onClick, { useHotkeys(paneConfig.keybinding, onClick, {
scopes: ['modeling'], scopes: ['modeling'],
}) })
@ -379,7 +383,7 @@ function ModelingPaneButton({
{paneIsOpen !== undefined ? ` pane` : ''} {paneIsOpen !== undefined ? ` pane` : ''}
</span> </span>
<kbd className="hotkey text-xs capitalize"> <kbd className="hotkey text-xs capitalize">
{paneConfig.keybinding} {hotkeyDisplay(paneConfig.keybinding, platform)}
</kbd> </kbd>
</Tooltip> </Tooltip>
</button> </button>

View File

@ -1,90 +0,0 @@
import React, { useMemo } from 'react'
import toast from 'react-hot-toast'
import { CustomIcon } from '@src/components/CustomIcon'
import Tooltip from '@src/components/Tooltip'
import { useMenuListener } from '@src/hooks/useMenu'
import { coreDump } from '@src/lang/wasm'
import { CoreDumpManager } from '@src/lib/coredump'
import {
codeManager,
engineCommandManager,
rustContext,
} from '@src/lib/singletons'
import { reportRejection } from '@src/lib/trap'
import { toSync } from '@src/lib/utils'
import { useToken } from '@src/machines/appMachine'
import type { WebContentSendPayload } from '@src/menu/channels'
export const RefreshButton = ({ children }: React.PropsWithChildren) => {
const token = useToken()
const coreDumpManager = useMemo(
() =>
new CoreDumpManager(
engineCommandManager,
codeManager,
rustContext,
token
),
[]
)
async function refresh() {
if (window && 'plausible' in window) {
const p = window.plausible as (
event: string,
options?: { props: Record<string, string> }
) => Promise<void>
// Send a refresh event to Plausible so we can track how often users get stuck
await p('Refresh', {
props: {
method: 'UI button',
// TODO: add more coredump data here
},
})
}
toast
.promise(
coreDump(coreDumpManager, true),
{
loading: 'Starting core dump...',
success: 'Core dump completed successfully',
error: 'Error while exporting core dump',
},
{
success: {
// Note: this extended duration is especially important for Playwright e2e testing
// default duration is 2000 - https://react-hot-toast.com/docs/toast#default-durations
duration: 6000,
},
}
)
.then(() => {
// Window may not be available in some environments
window?.location.reload()
})
.catch(reportRejection)
}
const cb = (data: WebContentSendPayload) => {
if (data.menuLabel === 'Help.Refresh and report a bug') {
refresh().catch(reportRejection)
}
}
useMenuListener(cb)
return (
<button
onClick={toSync(refresh, reportRejection)}
className="p-1 m-0 bg-chalkboard-10/80 dark:bg-chalkboard-100/50 hover:bg-chalkboard-10 dark:hover:bg-chalkboard-100 rounded-full border border-solid border-chalkboard-20 dark:border-chalkboard-90"
>
<CustomIcon name="exclamationMark" className="w-5 h-5" />
<Tooltip position="bottom-right">
<span>Refresh and report</span>
<br />
<span className="text-xs">Send us data on how you got stuck</span>
</Tooltip>
</button>
)
}

View File

@ -0,0 +1,41 @@
import { CustomIcon } from '@src/components/CustomIcon'
import Tooltip from '@src/components/Tooltip'
import usePlatform from '@src/hooks/usePlatform'
import { hotkeyDisplay } from '@src/lib/hotkeyWrapper'
import { commandBarActor } from '@src/machines/commandBarMachine'
import { useHotkeys } from 'react-hotkeys-hook'
const shareHotkey = 'mod+alt+s'
const onShareClick = () =>
commandBarActor.send({
type: 'Find and select command',
data: { name: 'share-file-link', groupId: 'code' },
})
/** Share Zoo link button shown in the upper-right of the modeling view */
export const ShareButton = () => {
const platform = usePlatform()
useHotkeys(shareHotkey, onShareClick, {
scopes: ['modeling'],
})
return (
<button
type="button"
onClick={onShareClick}
className="flex gap-1 items-center py-0 pl-0.5 pr-1.5 m-0 bg-chalkboard-10/80 dark:bg-chalkboard-100/50 hover:bg-chalkboard-10 dark:hover:bg-chalkboard-100 border border-solid active:border-primary"
>
<CustomIcon name="link" className="w-5 h-5" />
<span className="flex-1">Share</span>
<Tooltip
position="bottom-right"
contentClassName="max-w-none flex items-center gap-4"
>
<span className="flex-1">Share part via Zoo link</span>
<kbd className="hotkey text-xs capitalize">
{hotkeyDisplay(shareHotkey, platform)}
</kbd>
</Tooltip>
</button>
)
}

View File

@ -186,7 +186,7 @@ const UserSidebarMenu = ({ user }: { user?: User }) => {
) : ( ) : (
<CustomIcon <CustomIcon
name="person" name="person"
className="w-5 h-5 text-chalkboard-70 dark:text-chalkboard-40 bg-chalkboard-20 dark:bg-chalkboard-80" className="w-7 h-7 text-chalkboard-70 dark:text-chalkboard-40 bg-chalkboard-20 dark:bg-chalkboard-80"
/> />
)} )}
</div> </div>

View File

@ -1,6 +1,8 @@
import type { Command } from '@src/lib/commandTypes' import type { Command } from '@src/lib/commandTypes'
import { authActor } from '@src/machines/appMachine' import { authActor } from '@src/machines/appMachine'
import { ACTOR_IDS } from '@src/machines/machineConstants' import { ACTOR_IDS } from '@src/machines/machineConstants'
import { refreshPage } from '@src/lib/utils'
import { reportRejection } from '@src/lib/trap'
export const authCommands: Command[] = [ export const authCommands: Command[] = [
{ {
@ -11,4 +13,14 @@ export const authCommands: Command[] = [
needsReview: false, needsReview: false,
onSubmit: () => authActor.send({ type: 'Log out' }), onSubmit: () => authActor.send({ type: 'Log out' }),
}, },
{
groupId: ACTOR_IDS.AUTH,
name: 'refresh',
displayName: 'Refresh app',
icon: 'arrowRotateRight',
needsReview: false,
onSubmit: () => {
refreshPage('Command palette').catch(reportRejection)
},
},
] ]

View File

@ -60,6 +60,7 @@ export function hotkeyDisplay(hotkey: string, platform: Platform): string {
// Capitalize letters. We want Ctrl+K, not Ctrl+k, since Shift should be // Capitalize letters. We want Ctrl+K, not Ctrl+k, since Shift should be
// shown as a separate modifier. // shown as a separate modifier.
.split('+') .split('+')
.map((word) => word.trim().toLocaleLowerCase())
.map((word) => { .map((word) => {
if (word.length === 1 && LOWER_CASE_LETTER.test(word)) { if (word.length === 1 && LOWER_CASE_LETTER.test(word)) {
return word.toUpperCase() return word.toUpperCase()

View File

@ -8,6 +8,28 @@ import type { AsyncFn } from '@src/lib/types'
export const uuidv4 = v4 export const uuidv4 = v4
/**
* Refresh the browser page after reporting to Plausible.
*/
export async function refreshPage(method = 'UI button') {
if (window && 'plausible' in window) {
const p = window.plausible as (
event: string,
options?: { props: Record<string, string> }
) => Promise<void>
// Send a refresh event to Plausible so we can track how often users get stuck
await p('Refresh', {
props: {
method,
// optionally add more data here
},
})
}
// Window may not be available in some environments
window?.location.reload()
}
/** /**
* Get all labels for a keyword call expression. * Get all labels for a keyword call expression.
*/ */

View File

@ -5,7 +5,7 @@ import type { Channel } from '@src/channels'
// types for knowing what menu sends what webContent payload // types for knowing what menu sends what webContent payload
export type MenuLabels = export type MenuLabels =
| 'Help.Command Palette...' | 'Help.Command Palette...'
| 'Help.Refresh and report a bug' | 'Help.Report a bug'
| 'Help.Reset onboarding' | 'Help.Reset onboarding'
| 'Edit.Rename project' | 'Edit.Rename project'
| 'Edit.Delete project' | 'Edit.Delete project'

View File

@ -62,12 +62,14 @@ export const helpRole = (
}, },
{ type: 'separator' }, { type: 'separator' },
{ {
label: 'Refresh and report a bug', label: 'Report a bug',
id: 'Help.Refresh and report a bug', id: 'Help.Report a bug',
click: () => { click: () => {
typeSafeWebContentsSend(mainWindow, 'menu-action-clicked', { shell
menuLabel: 'Help.Refresh and report a bug', .openExternal(
}) 'https://github.com/KittyCAD/modeling-app/issues/new?template=bug_report.yml'
)
.catch(reportRejection)
}, },
}, },
{ {

View File

@ -39,7 +39,7 @@ type EditRoleLabel =
| 'Format code' | 'Format code'
type HelpRoleLabel = type HelpRoleLabel =
| 'Refresh and report a bug' | 'Report a bug'
| 'Request a feature' | 'Request a feature'
| 'Ask the community discord' | 'Ask the community discord'
| 'Ask the community discourse' | 'Ask the community discourse'