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
@ -409,11 +409,7 @@ test.describe('Native file menu', { tag: ['@electron'] }, () => {
|
||||
)
|
||||
.toBe(true)
|
||||
})
|
||||
test('Home.Help.Refresh and report a bug', async ({
|
||||
tronApp,
|
||||
cmdBar,
|
||||
page,
|
||||
}) => {
|
||||
test('Home.Help.Report a bug', async ({ tronApp, cmdBar, page }) => {
|
||||
if (!tronApp) fail()
|
||||
// Run electron snippet to find the Menu!
|
||||
await page.waitForTimeout(100) // wait for createModelingPageMenu() to run
|
||||
@ -424,9 +420,8 @@ test.describe('Native file menu', { tag: ['@electron'] }, () => {
|
||||
if (!app || !app.applicationMenu) {
|
||||
return false
|
||||
}
|
||||
const menu = app.applicationMenu.getMenuItemById(
|
||||
'Help.Refresh and report a bug'
|
||||
)
|
||||
const menu =
|
||||
app.applicationMenu.getMenuItemById('Help.Report a bug')
|
||||
if (!menu) return false
|
||||
menu.click()
|
||||
return true
|
||||
@ -2291,7 +2286,7 @@ test.describe('Native file menu', { tag: ['@electron'] }, () => {
|
||||
if (!menu) fail()
|
||||
})
|
||||
})
|
||||
test('Modeling.Help.Refresh and report a bug', async ({
|
||||
test('Modeling.Help.Report a bug', async ({
|
||||
tronApp,
|
||||
cmdBar,
|
||||
page,
|
||||
@ -2315,9 +2310,8 @@ test.describe('Native file menu', { tag: ['@electron'] }, () => {
|
||||
async () =>
|
||||
await tronApp.electron.evaluate(async ({ app }) => {
|
||||
if (!app || !app.applicationMenu) return false
|
||||
const menu = app.applicationMenu.getMenuItemById(
|
||||
'Help.Refresh and report a bug'
|
||||
)
|
||||
const menu =
|
||||
app.applicationMenu.getMenuItemById('Help.Report a bug')
|
||||
if (!menu) return false
|
||||
menu.click()
|
||||
return true
|
||||
|
@ -400,11 +400,6 @@ test(
|
||||
await expect(page.getByText('broken-code')).toBeVisible()
|
||||
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
|
||||
await editor.scrollToText(
|
||||
"|> line(end = [0, wallMountL], tag = 'outerEdge')"
|
||||
@ -779,7 +774,9 @@ test.describe(`Project management commands`, () => {
|
||||
// Constants and locators
|
||||
const projectHomeLink = page.getByTestId('project-link')
|
||||
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 projectRenamedName = `untitled`
|
||||
// const projectMenuButton = page.getByTestId('project-sidebar-toggle')
|
||||
@ -839,7 +836,9 @@ test.describe(`Project management commands`, () => {
|
||||
// Constants and locators
|
||||
const projectHomeLink = page.getByTestId('project-link')
|
||||
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 commandWarning = page.getByText('Are you sure you want to delete?')
|
||||
const commandSubmitButton = page.getByRole('button', {
|
||||
@ -891,7 +890,9 @@ test.describe(`Project management commands`, () => {
|
||||
// Constants and locators
|
||||
const projectHomeLink = page.getByTestId('project-link')
|
||||
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 projectRenamedName = `untitled`
|
||||
const commandContinueButton = page.getByRole('button', {
|
||||
@ -947,7 +948,9 @@ test.describe(`Project management commands`, () => {
|
||||
// Constants and locators
|
||||
const projectHomeLink = page.getByTestId('project-link')
|
||||
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 commandWarning = page.getByText('Are you sure you want to delete?')
|
||||
const commandSubmitButton = page.getByRole('button', {
|
||||
|
Before Width: | Height: | Size: 57 KiB After Width: | Height: | Size: 57 KiB |
Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 50 KiB |
Before Width: | Height: | Size: 57 KiB After Width: | Height: | Size: 58 KiB |
Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 50 KiB |
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 49 KiB |
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 47 KiB |
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 44 KiB |
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 47 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: 68 KiB |
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 30 KiB |
Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 49 KiB |
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 48 KiB |
Before Width: | Height: | Size: 75 KiB After Width: | Height: | Size: 75 KiB |
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 46 KiB |
Before Width: | Height: | Size: 59 KiB After Width: | Height: | Size: 59 KiB |
Before Width: | Height: | Size: 134 KiB After Width: | Height: | Size: 134 KiB |
Before Width: | Height: | Size: 117 KiB After Width: | Height: | Size: 117 KiB |
Before Width: | Height: | Size: 66 KiB After Width: | Height: | Size: 66 KiB |
Before Width: | Height: | Size: 69 KiB After Width: | Height: | Size: 69 KiB |
Before Width: | Height: | Size: 65 KiB After Width: | Height: | Size: 65 KiB |
Before Width: | Height: | Size: 67 KiB After Width: | Height: | Size: 68 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 |
@ -36,7 +36,6 @@ export const headerMasks = (page: Page) => [
|
||||
]
|
||||
|
||||
export const networkingMasks = (page: Page) => [
|
||||
page.getByTestId('model-state-indicator'),
|
||||
page.getByTestId('network-toggle'),
|
||||
]
|
||||
|
||||
@ -85,12 +84,6 @@ async function waitForPageLoadWithRetry(page: Page) {
|
||||
await expect(async () => {
|
||||
await page.goto('/')
|
||||
const errorMessage = 'App failed to load - 🔃 Retrying ...'
|
||||
await expect(
|
||||
page.getByTestId('model-state-indicator-playing'),
|
||||
errorMessage
|
||||
).toBeAttached({
|
||||
timeout: 20_000,
|
||||
})
|
||||
|
||||
await expect(
|
||||
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.
|
||||
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({
|
||||
timeout: 20_000,
|
||||
})
|
||||
|
34
src/App.tsx
@ -1,4 +1,4 @@
|
||||
import { useEffect, useMemo, useRef } from 'react'
|
||||
import { useEffect, useRef } from 'react'
|
||||
import toast from 'react-hot-toast'
|
||||
import { useHotkeys } from 'react-hotkeys-hook'
|
||||
import ModalContainer from 'react-modal-promise'
|
||||
@ -21,20 +21,14 @@ import { useAbsoluteFilePath } from '@src/hooks/useAbsoluteFilePath'
|
||||
import { useCreateFileLinkQuery } from '@src/hooks/useCreateFileLinkQueryWatcher'
|
||||
import { useEngineConnectionSubscriptions } from '@src/hooks/useEngineConnectionSubscriptions'
|
||||
import { useHotKeyListener } from '@src/hooks/useHotKeyListener'
|
||||
import { CoreDumpManager } from '@src/lib/coredump'
|
||||
import { writeProjectThumbnailFile } from '@src/lib/desktop'
|
||||
import useHotkeyWrapper from '@src/lib/hotkeyWrapper'
|
||||
import { isDesktop } from '@src/lib/isDesktop'
|
||||
import { PATHS } from '@src/lib/paths'
|
||||
import { takeScreenshotOfVideoStreamCanvas } from '@src/lib/screenshot'
|
||||
import {
|
||||
codeManager,
|
||||
engineCommandManager,
|
||||
rustContext,
|
||||
sceneInfra,
|
||||
} from '@src/lib/singletons'
|
||||
import { sceneInfra } from '@src/lib/singletons'
|
||||
import { maybeWriteToDisk } from '@src/lib/telemetry'
|
||||
import { type IndexLoaderData } from '@src/lib/types'
|
||||
import type { IndexLoaderData } from '@src/lib/types'
|
||||
import {
|
||||
engineStreamActor,
|
||||
useSettings,
|
||||
@ -43,6 +37,8 @@ import {
|
||||
import { commandBarActor } from '@src/machines/commandBarMachine'
|
||||
import { EngineStreamTransition } from '@src/machines/engineStreamMachine'
|
||||
import { onboardingPaths } from '@src/routes/Onboarding/paths'
|
||||
import { CommandBarOpenButton } from '@src/components/CommandBarOpenButton'
|
||||
import { ShareButton } from '@src/components/ShareButton'
|
||||
|
||||
// CYCLIC REF
|
||||
sceneInfra.camControls.engineStreamActor = engineStreamActor
|
||||
@ -93,17 +89,6 @@ export function App() {
|
||||
const settings = useSettings()
|
||||
const authToken = useToken()
|
||||
|
||||
const coreDumpManager = useMemo(
|
||||
() =>
|
||||
new CoreDumpManager(
|
||||
engineCommandManager,
|
||||
codeManager,
|
||||
rustContext,
|
||||
authToken
|
||||
),
|
||||
[]
|
||||
)
|
||||
|
||||
const {
|
||||
app: { onboardingStatus },
|
||||
} = settings
|
||||
@ -163,15 +148,18 @@ export function App() {
|
||||
return (
|
||||
<div className="relative h-full flex flex-col" ref={ref}>
|
||||
<AppHeader
|
||||
className={'transition-opacity transition-duration-75 ' + paneOpacity}
|
||||
className={`transition-opacity transition-duration-75 ${paneOpacity}`}
|
||||
project={{ project, file }}
|
||||
enableMenu={true}
|
||||
/>
|
||||
>
|
||||
<CommandBarOpenButton />
|
||||
<ShareButton />
|
||||
</AppHeader>
|
||||
<ModalContainer />
|
||||
<ModelingSidebar paneOpacity={paneOpacity} />
|
||||
<EngineStream pool={pool} authToken={authToken} />
|
||||
{/* <CamToggle /> */}
|
||||
<LowerRightControls coreDumpManager={coreDumpManager}>
|
||||
<LowerRightControls>
|
||||
<UnitsMenu />
|
||||
<Gizmo />
|
||||
</LowerRightControls>
|
||||
|
@ -1,7 +1,6 @@
|
||||
import { Toolbar } from '@src/Toolbar'
|
||||
import { CommandBarOpenButton } from '@src/components/CommandBarOpenButton'
|
||||
import ProjectSidebarMenu from '@src/components/ProjectSidebarMenu'
|
||||
import { RefreshButton } from '@src/components/RefreshButton'
|
||||
import UserSidebarMenu from '@src/components/UserSidebarMenu'
|
||||
import { isDesktop } from '@src/lib/isDesktop'
|
||||
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">
|
||||
{showToolbar && <Toolbar />}
|
||||
</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 */}
|
||||
{children || (
|
||||
<>
|
||||
<CommandBarOpenButton />
|
||||
<RefreshButton />
|
||||
</>
|
||||
)}
|
||||
{children || <CommandBarOpenButton />}
|
||||
<UserSidebarMenu user={user} />
|
||||
</div>
|
||||
</header>
|
||||
|
@ -2,18 +2,21 @@ import { COMMAND_PALETTE_HOTKEY } from '@src/components/CommandBar/CommandBar'
|
||||
import usePlatform from '@src/hooks/usePlatform'
|
||||
import { hotkeyDisplay } from '@src/lib/hotkeyWrapper'
|
||||
import { commandBarActor } from '@src/machines/commandBarMachine'
|
||||
import { CustomIcon } from '@src/components/CustomIcon'
|
||||
|
||||
export function CommandBarOpenButton() {
|
||||
const platform = usePlatform()
|
||||
|
||||
return (
|
||||
<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' })}
|
||||
data-testid="command-bar-open-button"
|
||||
>
|
||||
<CustomIcon name="command" className="w-5 h-5" />
|
||||
<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)}
|
||||
</kbd>
|
||||
</button>
|
||||
|
@ -311,6 +311,22 @@ const CustomIconMap = {
|
||||
/>
|
||||
</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: (
|
||||
<svg
|
||||
viewBox="0 0 20 20"
|
||||
|
@ -1,26 +1,19 @@
|
||||
import toast from 'react-hot-toast'
|
||||
import { Link, useLocation } from 'react-router-dom'
|
||||
|
||||
import { CustomIcon } from '@src/components/CustomIcon'
|
||||
import { HelpMenu } from '@src/components/HelpMenu'
|
||||
import { ModelStateIndicator } from '@src/components/ModelStateIndicator'
|
||||
import { NetworkHealthIndicator } from '@src/components/NetworkHealthIndicator'
|
||||
import { NetworkMachineIndicator } from '@src/components/NetworkMachineIndicator'
|
||||
import Tooltip from '@src/components/Tooltip'
|
||||
import { useAbsoluteFilePath } from '@src/hooks/useAbsoluteFilePath'
|
||||
import { coreDump } from '@src/lang/wasm'
|
||||
import type { CoreDumpManager } from '@src/lib/coredump'
|
||||
import openWindow, { openExternalBrowserIfDesktop } from '@src/lib/openWindow'
|
||||
import { openExternalBrowserIfDesktop } from '@src/lib/openWindow'
|
||||
import { PATHS } from '@src/lib/paths'
|
||||
import { reportRejection } from '@src/lib/trap'
|
||||
import { APP_VERSION, getReleaseUrl } from '@src/routes/utils'
|
||||
|
||||
export function LowerRightControls({
|
||||
children,
|
||||
coreDumpManager,
|
||||
}: {
|
||||
children?: React.ReactNode
|
||||
coreDumpManager?: CoreDumpManager
|
||||
}) {
|
||||
const location = useLocation()
|
||||
const filePath = useAbsoluteFilePath()
|
||||
@ -28,50 +21,10 @@ export function LowerRightControls({
|
||||
const linkOverrideClassName =
|
||||
'!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 (
|
||||
<section className="fixed bottom-2 right-2 flex flex-col items-end gap-3 pointer-events-none">
|
||||
{children}
|
||||
<menu className="flex items-center justify-end gap-3 pointer-events-auto">
|
||||
{!location.pathname.startsWith(PATHS.HOME) && <ModelStateIndicator />}
|
||||
<a
|
||||
onClick={openExternalBrowserIfDesktop(getReleaseUrl())}
|
||||
href={getReleaseUrl()}
|
||||
@ -81,20 +34,6 @@ export function LowerRightControls({
|
||||
>
|
||||
v{APP_VERSION}
|
||||
</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
|
||||
to={
|
||||
location.pathname.includes(PATHS.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>
|
||||
)
|
||||
}
|
@ -25,6 +25,10 @@ import { isDesktop } from '@src/lib/isDesktop'
|
||||
import { useSettings } from '@src/machines/appMachine'
|
||||
import { commandBarActor } from '@src/machines/commandBarMachine'
|
||||
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 {
|
||||
paneOpacity: '' | 'opacity-20' | 'opacity-40'
|
||||
@ -86,18 +90,6 @@ export function ModelingSidebar({ paneOpacity }: ModelingSidebarProps) {
|
||||
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',
|
||||
title: 'Export part',
|
||||
@ -130,6 +122,17 @@ export function ModelingSidebar({ paneOpacity }: ModelingSidebarProps) {
|
||||
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(
|
||||
(action) =>
|
||||
@ -340,6 +343,7 @@ function ModelingPaneButton({
|
||||
disabledText,
|
||||
...props
|
||||
}: ModelingPaneButtonProps) {
|
||||
const platform = usePlatform()
|
||||
useHotkeys(paneConfig.keybinding, onClick, {
|
||||
scopes: ['modeling'],
|
||||
})
|
||||
@ -379,7 +383,7 @@ function ModelingPaneButton({
|
||||
{paneIsOpen !== undefined ? ` pane` : ''}
|
||||
</span>
|
||||
<kbd className="hotkey text-xs capitalize">
|
||||
{paneConfig.keybinding}
|
||||
{hotkeyDisplay(paneConfig.keybinding, platform)}
|
||||
</kbd>
|
||||
</Tooltip>
|
||||
</button>
|
||||
|
@ -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>
|
||||
)
|
||||
}
|
41
src/components/ShareButton.tsx
Normal 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>
|
||||
)
|
||||
}
|
@ -186,7 +186,7 @@ const UserSidebarMenu = ({ user }: { user?: User }) => {
|
||||
) : (
|
||||
<CustomIcon
|
||||
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>
|
||||
|
@ -1,6 +1,8 @@
|
||||
import type { Command } from '@src/lib/commandTypes'
|
||||
import { authActor } from '@src/machines/appMachine'
|
||||
import { ACTOR_IDS } from '@src/machines/machineConstants'
|
||||
import { refreshPage } from '@src/lib/utils'
|
||||
import { reportRejection } from '@src/lib/trap'
|
||||
|
||||
export const authCommands: Command[] = [
|
||||
{
|
||||
@ -11,4 +13,14 @@ export const authCommands: Command[] = [
|
||||
needsReview: false,
|
||||
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)
|
||||
},
|
||||
},
|
||||
]
|
||||
|
@ -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
|
||||
// shown as a separate modifier.
|
||||
.split('+')
|
||||
.map((word) => word.trim().toLocaleLowerCase())
|
||||
.map((word) => {
|
||||
if (word.length === 1 && LOWER_CASE_LETTER.test(word)) {
|
||||
return word.toUpperCase()
|
||||
|
@ -8,6 +8,28 @@ import type { AsyncFn } from '@src/lib/types'
|
||||
|
||||
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.
|
||||
*/
|
||||
|
@ -5,7 +5,7 @@ import type { Channel } from '@src/channels'
|
||||
// types for knowing what menu sends what webContent payload
|
||||
export type MenuLabels =
|
||||
| 'Help.Command Palette...'
|
||||
| 'Help.Refresh and report a bug'
|
||||
| 'Help.Report a bug'
|
||||
| 'Help.Reset onboarding'
|
||||
| 'Edit.Rename project'
|
||||
| 'Edit.Delete project'
|
||||
|
@ -62,12 +62,14 @@ export const helpRole = (
|
||||
},
|
||||
{ type: 'separator' },
|
||||
{
|
||||
label: 'Refresh and report a bug',
|
||||
id: 'Help.Refresh and report a bug',
|
||||
label: 'Report a bug',
|
||||
id: 'Help.Report a bug',
|
||||
click: () => {
|
||||
typeSafeWebContentsSend(mainWindow, 'menu-action-clicked', {
|
||||
menuLabel: 'Help.Refresh and report a bug',
|
||||
})
|
||||
shell
|
||||
.openExternal(
|
||||
'https://github.com/KittyCAD/modeling-app/issues/new?template=bug_report.yml'
|
||||
)
|
||||
.catch(reportRejection)
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -39,7 +39,7 @@ type EditRoleLabel =
|
||||
| 'Format code'
|
||||
|
||||
type HelpRoleLabel =
|
||||
| 'Refresh and report a bug'
|
||||
| 'Report a bug'
|
||||
| 'Request a feature'
|
||||
| 'Ask the community discord'
|
||||
| 'Ask the community discourse'
|
||||
|