Feature: Traditional menu actions in desktop application (#5892)
* chore: skeleton for building and creating menus. Need electron to renderer interface to dynamically set the menu * chore: skeleton typing for communication between nodes and web side * chore: more skeleton for the different roles within the menu options, need more type safety * chore: adding more skeleton and templates of what the menus could be * chore: implemented first pass for helpRole links * fix: syntax issue stopped the build step * feature: loading different menus based on your page * feature: Home page file role implemented * chore: handling the build workflow for the signin page * fix: moving edit actionst to the edit menu * chore: adding preferences to the file role * chore: redoing help roles based on the question mark widget * fix: auto fmt * chore: examples of accelerator strings for Menu.MenuItems keyboard shortcuts! * chore: oddly specific toggle API for disabling MenuItems from JS. No rules! * fix: do not implement a custom label disable thingy, use id on menu and use the native APIga * fix: auto fmt * fix: adding some typechecks and auto fmt fixes * fix: trying to fix custom type? * fix: nvm we back, the lsp on my editor borked for a second * fix: adding one more level to the custom type for the labels * chore: cleaning up type definitions to read easier * fix: resolving yarn lint errors * chore: adding file sign out * chore: adding more file bar actions * chore: ready for PR draft * fix: preemptive GC collectoin bug fix if somehow a user interacts with a menu while it is being GCed * fix: linking the OG source * fix: set application menu to null to avoid default electron menu * chore: trying to add more typescript * chore: BIG workflow changes... better typing, less IPC junk * fix: remapping the icp functions to the cb option select... * chore: all og events are rehooked up with new workflow pattern * feat: adding more options to the native bar! * fix: todo * chore: cleaning up some menus and adding more * fix: desktop vs browser and lint errors * fix: typescript did not like sample electorn JS code for the basic templates with isMac conditionals... * fix: PR clean up * fix: more PR cleanup * A snapshot a day keeps the bugs away! 📷🐛 * fix: added the new help menu to the default sign in and modeling page * fix: disabled two menu actions within sign in page since they will not do anything. * A snapshot a day keeps the bugs away! 📷🐛 * A snapshot a day keeps the bugs away! 📷🐛 * fix: mergining main, auto fixes * A snapshot a day keeps the bugs away! 📷🐛 * A snapshot a day keeps the bugs away! 📷🐛 * A snapshot a day keeps the bugs away! 📷🐛 * A snapshot a day keeps the bugs away! 📷🐛 * fix: fixed ipc renderer off/remove listener bug * A snapshot a day keeps the bugs away! 📷🐛 * A snapshot a day keeps the bugs away! 📷🐛 * A snapshot a day keeps the bugs away! 📷🐛 * A snapshot a day keeps the bugs away! 📷🐛 * fix: report a bug to refresha and report a bug * fix: new type for webContents send payload that does not brick TS * fix: removing import file from url since it is not working in the command palette for manual user input * fix: removing old comment * A snapshot a day keeps the bugs away! 📷🐛 * A snapshot a day keeps the bugs away! 📷🐛 * A snapshot a day keeps the bugs away! 📷🐛 * A snapshot a day keeps the bugs away! 📷🐛 * A snapshot a day keeps the bugs away! 📷🐛 * A snapshot a day keeps the bugs away! 📷🐛 * A snapshot a day keeps the bugs away! 📷🐛 * A snapshot a day keeps the bugs away! 📷🐛 * A snapshot a day keeps the bugs away! 📷🐛 * chore: adding some E2E tests. * chore: added E2E tests for each file menu * fix: auto fixes * chore: adding more edit role E2E tests * chore: e2e test * chore: adding help role e2e test * A snapshot a day keeps the bugs away! 📷🐛 * chore: e2e test for all the menu options you can interact with in the frontend --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
This commit is contained in:
312
e2e/playwright/native-file-menu.spec.ts
Normal file
312
e2e/playwright/native-file-menu.spec.ts
Normal file
@ -0,0 +1,312 @@
|
|||||||
|
import { test, expect } from './zoo-test'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Not all menu actions are tested. Some are default electron menu actions.
|
||||||
|
* Test file menu actions that trigger something in the frontend
|
||||||
|
*/
|
||||||
|
test.describe('Native file menu', { tag: ['@electron'] }, () => {
|
||||||
|
test.describe('Home page', () => {
|
||||||
|
test.describe('File role', () => {
|
||||||
|
test('File.Create project', async ({ tronApp, cmdBar, page }) => {
|
||||||
|
if (!tronApp) fail()
|
||||||
|
// Run electron snippet to find the Menu!
|
||||||
|
await tronApp.electron.evaluate(async ({ app }) => {
|
||||||
|
if (!app || !app.applicationMenu) fail()
|
||||||
|
const newProject =
|
||||||
|
app.applicationMenu.getMenuItemById('File.New project')
|
||||||
|
if (!newProject) fail()
|
||||||
|
newProject.click()
|
||||||
|
})
|
||||||
|
// Check that the command bar is opened
|
||||||
|
await expect(cmdBar.cmdBarElement).toBeVisible()
|
||||||
|
// Check the placeholder project name exists
|
||||||
|
const actualArgument = await cmdBar.cmdBarElement
|
||||||
|
.getByTestId('cmd-bar-arg-value')
|
||||||
|
.inputValue()
|
||||||
|
const expectedArgument = 'project-$nnn'
|
||||||
|
expect(actualArgument).toBe(expectedArgument)
|
||||||
|
})
|
||||||
|
test('File.Open project', async ({ tronApp, cmdBar, page }) => {
|
||||||
|
if (!tronApp) fail()
|
||||||
|
// Run electron snippet to find the Menu!
|
||||||
|
await tronApp.electron.evaluate(async ({ app }) => {
|
||||||
|
if (!app || !app.applicationMenu) fail()
|
||||||
|
const openProject =
|
||||||
|
app.applicationMenu.getMenuItemById('File.Open project')
|
||||||
|
if (!openProject) fail()
|
||||||
|
openProject.click()
|
||||||
|
})
|
||||||
|
// Check that the command bar is opened
|
||||||
|
await expect(cmdBar.cmdBarElement).toBeVisible()
|
||||||
|
// Check the placeholder project name exists
|
||||||
|
const actual = await cmdBar.cmdBarElement
|
||||||
|
.getByTestId('command-name')
|
||||||
|
.textContent()
|
||||||
|
const expected = 'Open project'
|
||||||
|
expect(actual).toBe(expected)
|
||||||
|
})
|
||||||
|
test('File.Preferences.User settings', async ({
|
||||||
|
tronApp,
|
||||||
|
cmdBar,
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
if (!tronApp) fail()
|
||||||
|
// Run electron snippet to find the Menu!
|
||||||
|
await tronApp.electron.evaluate(async ({ app }) => {
|
||||||
|
if (!app || !app.applicationMenu) fail()
|
||||||
|
const userSettings = app.applicationMenu.getMenuItemById(
|
||||||
|
'File.Preferences.User settings'
|
||||||
|
)
|
||||||
|
if (!userSettings) fail()
|
||||||
|
userSettings.click()
|
||||||
|
})
|
||||||
|
const settings = page.getByTestId('settings-dialog-panel')
|
||||||
|
await expect(settings).toBeVisible()
|
||||||
|
// You are viewing the user tab
|
||||||
|
const actualText = settings.getByText(
|
||||||
|
'The overall appearance of the app'
|
||||||
|
)
|
||||||
|
await expect(actualText).toBeVisible()
|
||||||
|
})
|
||||||
|
test('File.Preferences.Keybindings', async ({
|
||||||
|
tronApp,
|
||||||
|
cmdBar,
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
if (!tronApp) fail()
|
||||||
|
// Run electron snippet to find the Menu!
|
||||||
|
await tronApp.electron.evaluate(async ({ app }) => {
|
||||||
|
if (!app || !app.applicationMenu) fail()
|
||||||
|
const keybindings = app.applicationMenu.getMenuItemById(
|
||||||
|
'File.Preferences.Keybindings'
|
||||||
|
)
|
||||||
|
if (!keybindings) fail()
|
||||||
|
keybindings.click()
|
||||||
|
})
|
||||||
|
const settings = page.getByTestId('settings-dialog-panel')
|
||||||
|
await expect(settings).toBeVisible()
|
||||||
|
// You are viewing the keybindings tab
|
||||||
|
const enterSketchMode = settings.locator('#enter-sketch-mode')
|
||||||
|
await expect(enterSketchMode).toBeVisible()
|
||||||
|
})
|
||||||
|
test('File.Preferences.User default units', async ({
|
||||||
|
tronApp,
|
||||||
|
cmdBar,
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
if (!tronApp) fail()
|
||||||
|
// Run electron snippet to find the Menu!
|
||||||
|
await tronApp.electron.evaluate(async ({ app }) => {
|
||||||
|
if (!app || !app.applicationMenu) fail()
|
||||||
|
const menu = app.applicationMenu.getMenuItemById(
|
||||||
|
'File.Preferences.User default units'
|
||||||
|
)
|
||||||
|
if (!menu) fail()
|
||||||
|
menu.click()
|
||||||
|
})
|
||||||
|
const settings = page.getByTestId('settings-dialog-panel')
|
||||||
|
await expect(settings).toBeVisible()
|
||||||
|
const defaultUnit = settings.locator('#defaultUnit')
|
||||||
|
await expect(defaultUnit).toBeVisible()
|
||||||
|
})
|
||||||
|
test('File.Preferences.Theme', async ({ tronApp, cmdBar, page }) => {
|
||||||
|
if (!tronApp) fail()
|
||||||
|
// Run electron snippet to find the Menu!
|
||||||
|
await tronApp.electron.evaluate(async ({ app }) => {
|
||||||
|
if (!app || !app.applicationMenu) fail()
|
||||||
|
const menu = app.applicationMenu.getMenuItemById(
|
||||||
|
'File.Preferences.Theme'
|
||||||
|
)
|
||||||
|
if (!menu) fail()
|
||||||
|
menu.click()
|
||||||
|
})
|
||||||
|
// Check that the command bar is opened
|
||||||
|
await expect(cmdBar.cmdBarElement).toBeVisible()
|
||||||
|
// Check the placeholder project name exists
|
||||||
|
const actual = await cmdBar.cmdBarElement
|
||||||
|
.getByTestId('command-name')
|
||||||
|
.textContent()
|
||||||
|
const expected = 'Settings · app · theme'
|
||||||
|
expect(actual).toBe(expected)
|
||||||
|
})
|
||||||
|
test('File.Preferences.Theme color', async ({
|
||||||
|
tronApp,
|
||||||
|
cmdBar,
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
if (!tronApp) fail()
|
||||||
|
// Run electron snippet to find the Menu!
|
||||||
|
await tronApp.electron.evaluate(async ({ app }) => {
|
||||||
|
if (!app || !app.applicationMenu) fail()
|
||||||
|
const menu = app.applicationMenu.getMenuItemById(
|
||||||
|
'File.Preferences.Theme color'
|
||||||
|
)
|
||||||
|
if (!menu) fail()
|
||||||
|
menu.click()
|
||||||
|
})
|
||||||
|
const settings = page.getByTestId('settings-dialog-panel')
|
||||||
|
await expect(settings).toBeVisible()
|
||||||
|
const defaultUnit = settings.locator('#themeColor')
|
||||||
|
await expect(defaultUnit).toBeVisible()
|
||||||
|
})
|
||||||
|
test('File.Preferences.Sign out', async ({ tronApp, cmdBar, page }) => {
|
||||||
|
if (!tronApp) fail()
|
||||||
|
// Run electron snippet to find the Menu!
|
||||||
|
await tronApp.electron.evaluate(async ({ app }) => {
|
||||||
|
if (!app || !app.applicationMenu) fail()
|
||||||
|
const menu = app.applicationMenu.getMenuItemById('File.Sign out')
|
||||||
|
if (!menu) fail()
|
||||||
|
// FIXME: Add back when you can actually sign out
|
||||||
|
// menu.click()
|
||||||
|
})
|
||||||
|
// FIXME: When signing out during E2E the page is not bound correctly.
|
||||||
|
// It cannot find the button
|
||||||
|
// const signIn = page.getByTestId('sign-in-button')
|
||||||
|
// await expect(signIn).toBeVisible()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test.describe('Edit role', () => {
|
||||||
|
test('Edit.Rename project', async ({ tronApp, cmdBar, page }) => {
|
||||||
|
if (!tronApp) fail()
|
||||||
|
// Run electron snippet to find the Menu!
|
||||||
|
await tronApp.electron.evaluate(async ({ app }) => {
|
||||||
|
if (!app || !app.applicationMenu) fail()
|
||||||
|
const menu = app.applicationMenu.getMenuItemById(
|
||||||
|
'Edit.Rename project'
|
||||||
|
)
|
||||||
|
if (!menu) fail()
|
||||||
|
menu.click()
|
||||||
|
})
|
||||||
|
// Check the placeholder project name exists
|
||||||
|
const actual = await cmdBar.cmdBarElement
|
||||||
|
.getByTestId('command-name')
|
||||||
|
.textContent()
|
||||||
|
const expected = 'Rename project'
|
||||||
|
expect(actual).toBe(expected)
|
||||||
|
})
|
||||||
|
test('Edit.Delete project', async ({ tronApp, cmdBar, page }) => {
|
||||||
|
if (!tronApp) fail()
|
||||||
|
// Run electron snippet to find the Menu!
|
||||||
|
await tronApp.electron.evaluate(async ({ app }) => {
|
||||||
|
if (!app || !app.applicationMenu) fail()
|
||||||
|
const menu = app.applicationMenu.getMenuItemById(
|
||||||
|
'Edit.Delete project'
|
||||||
|
)
|
||||||
|
if (!menu) fail()
|
||||||
|
menu.click()
|
||||||
|
})
|
||||||
|
// Check the placeholder project name exists
|
||||||
|
const actual = await cmdBar.cmdBarElement
|
||||||
|
.getByTestId('command-name')
|
||||||
|
.textContent()
|
||||||
|
const expected = 'Delete project'
|
||||||
|
expect(actual).toBe(expected)
|
||||||
|
})
|
||||||
|
test('Edit.Change project directory', async ({
|
||||||
|
tronApp,
|
||||||
|
cmdBar,
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
if (!tronApp) fail()
|
||||||
|
// Run electron snippet to find the Menu!
|
||||||
|
await tronApp.electron.evaluate(async ({ app }) => {
|
||||||
|
if (!app || !app.applicationMenu) fail()
|
||||||
|
const menu = app.applicationMenu.getMenuItemById(
|
||||||
|
'Edit.Change project directory'
|
||||||
|
)
|
||||||
|
if (!menu) fail()
|
||||||
|
menu.click()
|
||||||
|
})
|
||||||
|
const settings = page.getByTestId('settings-dialog-panel')
|
||||||
|
await expect(settings).toBeVisible()
|
||||||
|
const projectDirectory = settings.locator('#projectDirectory')
|
||||||
|
await expect(projectDirectory).toBeVisible()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
test.describe('View role', () => {
|
||||||
|
test('View.Command Palette...', async ({ tronApp, cmdBar, page }) => {
|
||||||
|
if (!tronApp) fail()
|
||||||
|
// Run electron snippet to find the Menu!
|
||||||
|
await tronApp.electron.evaluate(async ({ app }) => {
|
||||||
|
if (!app || !app.applicationMenu) fail()
|
||||||
|
const menu = app.applicationMenu.getMenuItemById(
|
||||||
|
'View.Command Palette...'
|
||||||
|
)
|
||||||
|
if (!menu) fail()
|
||||||
|
menu.click()
|
||||||
|
})
|
||||||
|
// Check the placeholder project name exists
|
||||||
|
const actual = cmdBar.cmdBarElement.getByTestId('cmd-bar-search')
|
||||||
|
await expect(actual).toBeVisible()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
test.describe('Help role', () => {
|
||||||
|
test('Help.Show all commands', async ({ tronApp, cmdBar, page }) => {
|
||||||
|
if (!tronApp) fail()
|
||||||
|
// Run electron snippet to find the Menu!
|
||||||
|
await tronApp.electron.evaluate(async ({ app }) => {
|
||||||
|
if (!app || !app.applicationMenu) fail()
|
||||||
|
const menu = app.applicationMenu.getMenuItemById(
|
||||||
|
'Help.Show all commands'
|
||||||
|
)
|
||||||
|
if (!menu) fail()
|
||||||
|
menu.click()
|
||||||
|
})
|
||||||
|
// Check the placeholder project name exists
|
||||||
|
const actual = cmdBar.cmdBarElement.getByTestId('cmd-bar-search')
|
||||||
|
await expect(actual).toBeVisible()
|
||||||
|
})
|
||||||
|
test('Help.KCL code samples', async ({ tronApp, cmdBar, page }) => {
|
||||||
|
if (!tronApp) fail()
|
||||||
|
// Run electron snippet to find the Menu!
|
||||||
|
await tronApp.electron.evaluate(async ({ app }) => {
|
||||||
|
if (!app || !app.applicationMenu) fail()
|
||||||
|
const menu = app.applicationMenu.getMenuItemById(
|
||||||
|
'Help.KCL code samples'
|
||||||
|
)
|
||||||
|
if (!menu) fail()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
test('Help.Refresh and report a bug', async ({
|
||||||
|
tronApp,
|
||||||
|
cmdBar,
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
if (!tronApp) fail()
|
||||||
|
// Run electron snippet to find the Menu!
|
||||||
|
await tronApp.electron.evaluate(async ({ app }) => {
|
||||||
|
if (!app || !app.applicationMenu) fail()
|
||||||
|
const menu = app.applicationMenu.getMenuItemById(
|
||||||
|
'Help.Refresh and report a bug'
|
||||||
|
)
|
||||||
|
if (!menu) fail()
|
||||||
|
menu.click()
|
||||||
|
})
|
||||||
|
// Core dump and refresh magic number timeout
|
||||||
|
await page.waitForTimeout(7000)
|
||||||
|
const actual = page.getByText(
|
||||||
|
'No Projects found, ready to make your first one?'
|
||||||
|
)
|
||||||
|
await expect(actual).toBeVisible()
|
||||||
|
})
|
||||||
|
test('Help.Reset onboarding', async ({ tronApp, cmdBar, page }) => {
|
||||||
|
if (!tronApp) fail()
|
||||||
|
// Run electron snippet to find the Menu!
|
||||||
|
await tronApp.electron.evaluate(async ({ app }) => {
|
||||||
|
if (!app || !app.applicationMenu) fail()
|
||||||
|
const menu = app.applicationMenu.getMenuItemById(
|
||||||
|
'Help.Reset onboarding'
|
||||||
|
)
|
||||||
|
if (!menu) fail()
|
||||||
|
menu.click()
|
||||||
|
})
|
||||||
|
|
||||||
|
const actual = page.getByText(
|
||||||
|
`This is a hardware design tool that lets you edit visually, with code, or both. It's powered by the KittyCAD Design API, the first API created for anyone to build hardware design tools.`
|
||||||
|
)
|
||||||
|
await expect(actual).toBeVisible()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
20
interface.d.ts
vendored
20
interface.d.ts
vendored
@ -3,6 +3,18 @@ import fsSync from 'node:fs'
|
|||||||
import path from 'path'
|
import path from 'path'
|
||||||
import { dialog, shell } from 'electron'
|
import { dialog, shell } from 'electron'
|
||||||
import { MachinesListing } from 'components/MachineManagerProvider'
|
import { MachinesListing } from 'components/MachineManagerProvider'
|
||||||
|
import type { Channel } from 'src/menu/channels'
|
||||||
|
import { Menu, WebContents } from 'electron'
|
||||||
|
import { ZooLabel, ZooMenuEvents } from 'menu/roles'
|
||||||
|
import type { MenuActionIPC } from 'menu/rules'
|
||||||
|
import type { WebContentSendPayload } from 'menu/channels'
|
||||||
|
|
||||||
|
// Extend the interface with additional custom properties
|
||||||
|
declare module 'electron' {
|
||||||
|
interface Menu {
|
||||||
|
label?: ZooLabel
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type EnvFn = (value?: string) => string
|
type EnvFn = (value?: string) => string
|
||||||
|
|
||||||
@ -94,6 +106,14 @@ export interface IElectronAPI {
|
|||||||
appCheckForUpdates: () => Promise<unknown>
|
appCheckForUpdates: () => Promise<unknown>
|
||||||
getArgvParsed: () => any
|
getArgvParsed: () => any
|
||||||
getAppTestProperty: (propertyName: string) => any
|
getAppTestProperty: (propertyName: string) => any
|
||||||
|
|
||||||
|
// Helper functions to create application Menus
|
||||||
|
createHomePageMenu: () => Promise<any>
|
||||||
|
createModelingPageMenu: () => Promise<any>
|
||||||
|
createFallbackMenu: () => Promise<any>
|
||||||
|
enableMenu(menuId: string): Promise<any>
|
||||||
|
disableMenu(menuId: string): Promise<any>
|
||||||
|
menuOn: (callback: (payload: WebContentSendPayload) => void) => any
|
||||||
}
|
}
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
|
1
src/channels.ts
Normal file
1
src/channels.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export type Channel = 'menu-action-clicked'
|
@ -33,6 +33,7 @@ import { settingsActor, useSettings } from 'machines/appMachine'
|
|||||||
import { createRouteCommands } from 'lib/commandBarConfigs/routeCommandConfig'
|
import { createRouteCommands } from 'lib/commandBarConfigs/routeCommandConfig'
|
||||||
import { useToken } from 'machines/appMachine'
|
import { useToken } from 'machines/appMachine'
|
||||||
import { createNamedViewsCommand } from 'lib/commandBarConfigs/namedViewsConfig'
|
import { createNamedViewsCommand } from 'lib/commandBarConfigs/namedViewsConfig'
|
||||||
|
import { reportRejection } from 'lib/trap'
|
||||||
|
|
||||||
type MachineContext<T extends AnyStateMachine> = {
|
type MachineContext<T extends AnyStateMachine> = {
|
||||||
state: StateFrom<T>
|
state: StateFrom<T>
|
||||||
@ -59,6 +60,13 @@ export const FileMachineProvider = ({
|
|||||||
[]
|
[]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Only create the native file menus on desktop
|
||||||
|
useEffect(() => {
|
||||||
|
if (isDesktop()) {
|
||||||
|
window.electron.createModelingPageMenu().catch(reportRejection)
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const {
|
const {
|
||||||
createNamedViewCommand,
|
createNamedViewCommand,
|
||||||
|
@ -9,6 +9,8 @@ import { useLspContext } from './LspProvider'
|
|||||||
import { openExternalBrowserIfDesktop } from 'lib/openWindow'
|
import { openExternalBrowserIfDesktop } from 'lib/openWindow'
|
||||||
import { reportRejection } from 'lib/trap'
|
import { reportRejection } from 'lib/trap'
|
||||||
import { settingsActor } from 'machines/appMachine'
|
import { settingsActor } from 'machines/appMachine'
|
||||||
|
import type { WebContentSendPayload } from '../menu/channels'
|
||||||
|
import { useMenuListener } from 'hooks/useMenu'
|
||||||
|
|
||||||
const HelpMenuDivider = () => (
|
const HelpMenuDivider = () => (
|
||||||
<div className="h-[1px] bg-chalkboard-110 dark:bg-chalkboard-80" />
|
<div className="h-[1px] bg-chalkboard-110 dark:bg-chalkboard-80" />
|
||||||
@ -21,6 +23,31 @@ export function HelpMenu(props: React.PropsWithChildren) {
|
|||||||
const isInProject = location.pathname.includes(PATHS.FILE)
|
const isInProject = location.pathname.includes(PATHS.FILE)
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
|
||||||
|
const resetOnboardingWorkflow = () => {
|
||||||
|
settingsActor.send({
|
||||||
|
type: 'set.app.onboardingStatus',
|
||||||
|
data: {
|
||||||
|
value: '',
|
||||||
|
level: 'user',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if (isInProject) {
|
||||||
|
navigate(filePath + PATHS.ONBOARDING.INDEX)
|
||||||
|
} else {
|
||||||
|
createAndOpenNewTutorialProject({
|
||||||
|
onProjectOpen,
|
||||||
|
navigate,
|
||||||
|
}).catch(reportRejection)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const cb = (data: WebContentSendPayload) => {
|
||||||
|
if (data.menuLabel === 'Help.Reset onboarding') {
|
||||||
|
resetOnboardingWorkflow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
useMenuListener(cb)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover className="relative">
|
<Popover className="relative">
|
||||||
<Popover.Button
|
<Popover.Button
|
||||||
@ -102,26 +129,7 @@ export function HelpMenu(props: React.PropsWithChildren) {
|
|||||||
>
|
>
|
||||||
Keyboard shortcuts
|
Keyboard shortcuts
|
||||||
</HelpMenuItem>
|
</HelpMenuItem>
|
||||||
<HelpMenuItem
|
<HelpMenuItem as="button" onClick={resetOnboardingWorkflow}>
|
||||||
as="button"
|
|
||||||
onClick={() => {
|
|
||||||
settingsActor.send({
|
|
||||||
type: 'set.app.onboardingStatus',
|
|
||||||
data: {
|
|
||||||
value: '',
|
|
||||||
level: 'user',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
if (isInProject) {
|
|
||||||
navigate(filePath + PATHS.ONBOARDING.INDEX)
|
|
||||||
} else {
|
|
||||||
createAndOpenNewTutorialProject({
|
|
||||||
onProjectOpen,
|
|
||||||
navigate,
|
|
||||||
}).catch(reportRejection)
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Reset onboarding
|
Reset onboarding
|
||||||
</HelpMenuItem>
|
</HelpMenuItem>
|
||||||
</Popover.Panel>
|
</Popover.Panel>
|
||||||
|
@ -9,6 +9,8 @@ import { reportRejection } from 'lib/trap'
|
|||||||
import { toSync } from 'lib/utils'
|
import { toSync } from 'lib/utils'
|
||||||
import { useToken } from 'machines/appMachine'
|
import { useToken } from 'machines/appMachine'
|
||||||
import { rustContext } from 'lib/singletons'
|
import { rustContext } from 'lib/singletons'
|
||||||
|
import type { WebContentSendPayload } from '../menu/channels'
|
||||||
|
import { useMenuListener } from 'hooks/useMenu'
|
||||||
|
|
||||||
export const RefreshButton = ({ children }: React.PropsWithChildren) => {
|
export const RefreshButton = ({ children }: React.PropsWithChildren) => {
|
||||||
const token = useToken()
|
const token = useToken()
|
||||||
@ -61,6 +63,13 @@ export const RefreshButton = ({ children }: React.PropsWithChildren) => {
|
|||||||
.catch(reportRejection)
|
.catch(reportRejection)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const cb = (data: WebContentSendPayload) => {
|
||||||
|
if (data.menuLabel === 'Help.Refresh and report a bug') {
|
||||||
|
refresh().catch(reportRejection)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
useMenuListener(cb)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
onClick={toSync(refresh, reportRejection)}
|
onClick={toSync(refresh, reportRejection)}
|
||||||
|
23
src/hooks/useMenu.ts
Normal file
23
src/hooks/useMenu.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import { useEffect } from 'react'
|
||||||
|
import type { WebContentSendPayload } from '../menu/channels'
|
||||||
|
import { isDesktop } from 'lib/isDesktop'
|
||||||
|
export function useMenuListener(
|
||||||
|
callback: (data: WebContentSendPayload) => void
|
||||||
|
) {
|
||||||
|
useEffect(() => {
|
||||||
|
const onDesktop = isDesktop()
|
||||||
|
if (!onDesktop) {
|
||||||
|
// NO OP for web
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const removeListener = window.electron.menuOn(callback)
|
||||||
|
return () => {
|
||||||
|
if (!onDesktop) {
|
||||||
|
// NO OP for web
|
||||||
|
return
|
||||||
|
}
|
||||||
|
removeListener()
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
}
|
57
src/main.ts
57
src/main.ts
@ -10,6 +10,7 @@ import {
|
|||||||
nativeTheme,
|
nativeTheme,
|
||||||
desktopCapturer,
|
desktopCapturer,
|
||||||
systemPreferences,
|
systemPreferences,
|
||||||
|
Menu,
|
||||||
screen,
|
screen,
|
||||||
} from 'electron'
|
} from 'electron'
|
||||||
import path from 'path'
|
import path from 'path'
|
||||||
@ -27,11 +28,26 @@ import {
|
|||||||
getPathOrUrlFromArgs,
|
getPathOrUrlFromArgs,
|
||||||
parseCLIArgs,
|
parseCLIArgs,
|
||||||
} from './commandLineArgs'
|
} from './commandLineArgs'
|
||||||
|
|
||||||
import * as packageJSON from '../package.json'
|
import * as packageJSON from '../package.json'
|
||||||
|
import {
|
||||||
|
buildAndSetMenuForFallback,
|
||||||
|
buildAndSetMenuForModelingPage,
|
||||||
|
buildAndSetMenuForProjectPage,
|
||||||
|
enableMenu,
|
||||||
|
disableMenu,
|
||||||
|
} from './menu'
|
||||||
|
|
||||||
let mainWindow: BrowserWindow | null = null
|
let mainWindow: BrowserWindow | null = null
|
||||||
|
|
||||||
|
// Preemptive code, GC may delete a menu while a user is using it as seen in VSCode
|
||||||
|
// as seen on https://github.com/microsoft/vscode/issues/55347
|
||||||
|
let oldMenus: Menu[] = []
|
||||||
|
const scheduleMenuGC = () => {
|
||||||
|
setTimeout(() => {
|
||||||
|
oldMenus = []
|
||||||
|
}, 10000)
|
||||||
|
}
|
||||||
|
|
||||||
// Check the command line arguments for a project path
|
// Check the command line arguments for a project path
|
||||||
const args = parseCLIArgs(process.argv)
|
const args = parseCLIArgs(process.argv)
|
||||||
|
|
||||||
@ -215,6 +231,8 @@ app.on('ready', (event, data) => {
|
|||||||
if (mainWindow) return
|
if (mainWindow) return
|
||||||
// Create the mainWindow
|
// Create the mainWindow
|
||||||
mainWindow = createWindow()
|
mainWindow = createWindow()
|
||||||
|
// Set menu application to null to avoid default electron menu
|
||||||
|
Menu.setApplicationMenu(null)
|
||||||
})
|
})
|
||||||
|
|
||||||
// For now there is no good reason to separate these out to another file(s)
|
// For now there is no good reason to separate these out to another file(s)
|
||||||
@ -386,6 +404,43 @@ ipcMain.handle('find_machine_api', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Given the route create the new context menu
|
||||||
|
// internal menu state will be reset since it creates a new one from
|
||||||
|
// the initial state
|
||||||
|
ipcMain.handle('create-menu', (event, data) => {
|
||||||
|
const page = data.page
|
||||||
|
|
||||||
|
if (!(page === 'project' || page === 'modeling' || page === 'fallback')) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store old menu in our array to avoid GC to collect the menu and crash
|
||||||
|
const oldMenu = Menu.getApplicationMenu()
|
||||||
|
if (oldMenu) {
|
||||||
|
oldMenus.push(oldMenu)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (page === 'project' && mainWindow) {
|
||||||
|
buildAndSetMenuForProjectPage(mainWindow)
|
||||||
|
} else if (page === 'modeling' && mainWindow) {
|
||||||
|
buildAndSetMenuForModelingPage(mainWindow)
|
||||||
|
} else if (page === 'fallback' && mainWindow) {
|
||||||
|
buildAndSetMenuForFallback(mainWindow)
|
||||||
|
}
|
||||||
|
|
||||||
|
scheduleMenuGC()
|
||||||
|
})
|
||||||
|
|
||||||
|
ipcMain.handle('enable-menu', (event, data) => {
|
||||||
|
const menuId = data.menuId
|
||||||
|
enableMenu(menuId)
|
||||||
|
})
|
||||||
|
|
||||||
|
ipcMain.handle('disable-menu', (event, data) => {
|
||||||
|
const menuId = data.menuId
|
||||||
|
disableMenu(menuId)
|
||||||
|
})
|
||||||
|
|
||||||
export function getAutoUpdater(): AppUpdater {
|
export function getAutoUpdater(): AppUpdater {
|
||||||
// Using destructuring to access autoUpdater due to the CommonJS module of 'electron-updater'.
|
// Using destructuring to access autoUpdater due to the CommonJS module of 'electron-updater'.
|
||||||
// It is a workaround for ESM compatibility issues, see https://github.com/electron-userland/electron-builder/issues/7976.
|
// It is a workaround for ESM compatibility issues, see https://github.com/electron-userland/electron-builder/issues/7976.
|
||||||
|
166
src/menu.ts
Normal file
166
src/menu.ts
Normal file
@ -0,0 +1,166 @@
|
|||||||
|
import { app, Menu, BrowserWindow } from 'electron'
|
||||||
|
import { projectFileRole } from 'menu/fileRole'
|
||||||
|
import { projectEditRole } from 'menu/editRole'
|
||||||
|
import { helpRole } from 'menu/helpRole'
|
||||||
|
import { projectViewRole } from 'menu/viewRole'
|
||||||
|
|
||||||
|
import os from 'node:os'
|
||||||
|
import { ZooMenuItemConstructorOptions } from 'menu/roles'
|
||||||
|
const isMac = os.platform() === 'darwin'
|
||||||
|
|
||||||
|
// Default electron menu.
|
||||||
|
export function buildAndSetMenuForFallback(mainWindow: BrowserWindow) {
|
||||||
|
const templateMac: ZooMenuItemConstructorOptions[] = [
|
||||||
|
{
|
||||||
|
// @ts-ignore cannot determine this since it is dynamic. It is still a string, not a problem.
|
||||||
|
label: app.name,
|
||||||
|
submenu: [
|
||||||
|
{ role: 'about' },
|
||||||
|
{ type: 'separator' },
|
||||||
|
{ role: 'services' },
|
||||||
|
{ type: 'separator' },
|
||||||
|
{ role: 'hide' },
|
||||||
|
{ role: 'hideOthers' },
|
||||||
|
{ role: 'unhide' },
|
||||||
|
{ type: 'separator' },
|
||||||
|
{ role: 'quit' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'File',
|
||||||
|
submenu: [{ role: 'close' }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Edit',
|
||||||
|
submenu: [
|
||||||
|
{ role: 'undo' },
|
||||||
|
{ role: 'redo' },
|
||||||
|
{ type: 'separator' },
|
||||||
|
{ role: 'cut' },
|
||||||
|
{ role: 'copy' },
|
||||||
|
{ role: 'paste' },
|
||||||
|
{ role: 'pasteAndMatchStyle' },
|
||||||
|
{ role: 'delete' },
|
||||||
|
{ role: 'selectAll' },
|
||||||
|
{ type: 'separator' },
|
||||||
|
{
|
||||||
|
label: 'Speech',
|
||||||
|
submenu: [{ role: 'startSpeaking' }, { role: 'stopSpeaking' }],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'View',
|
||||||
|
submenu: [
|
||||||
|
{ role: 'reload' },
|
||||||
|
{ role: 'forceReload' },
|
||||||
|
{ role: 'toggleDevTools' },
|
||||||
|
{ type: 'separator' },
|
||||||
|
{ role: 'resetZoom' },
|
||||||
|
{ role: 'zoomIn' },
|
||||||
|
{ role: 'zoomOut' },
|
||||||
|
{ type: 'separator' },
|
||||||
|
{ role: 'togglefullscreen' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Window',
|
||||||
|
submenu: [
|
||||||
|
{ role: 'minimize' },
|
||||||
|
{ role: 'zoom' },
|
||||||
|
{ type: 'separator' },
|
||||||
|
{ role: 'front' },
|
||||||
|
{ type: 'separator' },
|
||||||
|
{ role: 'window' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const templateNotMac: ZooMenuItemConstructorOptions[] = [
|
||||||
|
{
|
||||||
|
label: 'File',
|
||||||
|
submenu: [{ role: 'quit' }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Edit',
|
||||||
|
submenu: [
|
||||||
|
{ role: 'undo' },
|
||||||
|
{ role: 'redo' },
|
||||||
|
{ type: 'separator' },
|
||||||
|
{ role: 'cut' },
|
||||||
|
{ role: 'copy' },
|
||||||
|
{ role: 'paste' },
|
||||||
|
{ role: 'delete' },
|
||||||
|
{ type: 'separator' },
|
||||||
|
{ role: 'selectAll' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'View',
|
||||||
|
submenu: [
|
||||||
|
{ role: 'reload' },
|
||||||
|
{ role: 'forceReload' },
|
||||||
|
{ role: 'toggleDevTools' },
|
||||||
|
{ type: 'separator' },
|
||||||
|
{ role: 'resetZoom' },
|
||||||
|
{ role: 'zoomIn' },
|
||||||
|
{ role: 'zoomOut' },
|
||||||
|
{ type: 'separator' },
|
||||||
|
{ role: 'togglefullscreen' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
label: 'Window',
|
||||||
|
submenu: [{ role: 'minimize' }, { role: 'zoom' }, { role: 'close' }],
|
||||||
|
},
|
||||||
|
helpRole(mainWindow),
|
||||||
|
]
|
||||||
|
|
||||||
|
if (isMac) {
|
||||||
|
const menu = Menu.buildFromTemplate(templateMac)
|
||||||
|
Menu.setApplicationMenu(menu)
|
||||||
|
} else {
|
||||||
|
const menu = Menu.buildFromTemplate(templateNotMac)
|
||||||
|
Menu.setApplicationMenu(menu)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// This will generate a new menu from the initial state
|
||||||
|
// All state management from the previous menu is going to be lost.
|
||||||
|
export function buildAndSetMenuForModelingPage(mainWindow: BrowserWindow) {
|
||||||
|
return buildAndSetMenuForFallback(mainWindow)
|
||||||
|
}
|
||||||
|
|
||||||
|
// This will generate a new menu from the initial state
|
||||||
|
// All state management from the previous menu is going to be lost.
|
||||||
|
export function buildAndSetMenuForProjectPage(mainWindow: BrowserWindow) {
|
||||||
|
const template = [
|
||||||
|
projectFileRole(mainWindow),
|
||||||
|
projectEditRole(mainWindow),
|
||||||
|
projectViewRole(mainWindow),
|
||||||
|
// Help role is the same for all pages
|
||||||
|
helpRole(mainWindow),
|
||||||
|
]
|
||||||
|
const menu = Menu.buildFromTemplate(template)
|
||||||
|
Menu.setApplicationMenu(menu)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to enable the menu based on the application menu
|
||||||
|
// It will not do anything if that menu cannot be found.
|
||||||
|
export function enableMenu(menuId: string) {
|
||||||
|
const applicationMenu = Menu.getApplicationMenu()
|
||||||
|
const menuItem = applicationMenu?.getMenuItemById(menuId)
|
||||||
|
if (menuItem) {
|
||||||
|
menuItem.enabled = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to disable the menu based on the application menu
|
||||||
|
// It will not do anything if that menu cannot be found.
|
||||||
|
export function disableMenu(menuId: string) {
|
||||||
|
const applicationMenu = Menu.getApplicationMenu()
|
||||||
|
const menuItem = applicationMenu?.getMenuItemById(menuId)
|
||||||
|
if (menuItem) {
|
||||||
|
menuItem.enabled = false
|
||||||
|
}
|
||||||
|
}
|
37
src/menu/channels.ts
Normal file
37
src/menu/channels.ts
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import { BrowserWindow } from 'electron'
|
||||||
|
import type { Channel } from '../channels'
|
||||||
|
|
||||||
|
// types for knowing what menu sends what webContent payload
|
||||||
|
export type MenuLabels =
|
||||||
|
| 'Help.Command Palette...'
|
||||||
|
| 'Help.Refresh and report a bug'
|
||||||
|
| 'Help.Reset onboarding'
|
||||||
|
| 'File.New project'
|
||||||
|
| 'File.Open project'
|
||||||
|
| 'File.Import file from URL'
|
||||||
|
| 'File.Preferences.User settings'
|
||||||
|
| 'File.Preferences.Keybindings'
|
||||||
|
| 'File.Preferences.User default units'
|
||||||
|
| 'File.Preferences.Theme'
|
||||||
|
| 'File.Preferences.Theme color'
|
||||||
|
| 'File.Sign out'
|
||||||
|
| 'Edit.Rename project'
|
||||||
|
| 'Edit.Delete project'
|
||||||
|
| 'Edit.Change project directory'
|
||||||
|
| 'View.Command Palette...'
|
||||||
|
|
||||||
|
export type WebContentSendPayload = {
|
||||||
|
menuLabel: MenuLabels
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unable to use declare module 'electron' with the interface of WebContents
|
||||||
|
// to update the send function. It did not work.
|
||||||
|
// Need to use a custom wrapper function for this.
|
||||||
|
// BrowserWindow.webContents instance is different from the WebContents and webContents...?
|
||||||
|
export const typeSafeWebContentsSend = (
|
||||||
|
mainWindow: BrowserWindow,
|
||||||
|
channel: Channel,
|
||||||
|
payload: WebContentSendPayload
|
||||||
|
) => {
|
||||||
|
mainWindow.webContents.send(channel, payload)
|
||||||
|
}
|
68
src/menu/editRole.ts
Normal file
68
src/menu/editRole.ts
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
import { BrowserWindow } from 'electron'
|
||||||
|
import { typeSafeWebContentsSend } from './channels'
|
||||||
|
import os from 'node:os'
|
||||||
|
import { ZooMenuItemConstructorOptions } from './roles'
|
||||||
|
const isMac = os.platform() === 'darwin'
|
||||||
|
|
||||||
|
export const projectEditRole = (
|
||||||
|
mainWindow: BrowserWindow
|
||||||
|
): ZooMenuItemConstructorOptions => {
|
||||||
|
let extraBits: ZooMenuItemConstructorOptions[] = [
|
||||||
|
{ role: 'delete' },
|
||||||
|
{ type: 'separator' },
|
||||||
|
{ role: 'selectAll' },
|
||||||
|
]
|
||||||
|
if (isMac) {
|
||||||
|
extraBits = [
|
||||||
|
{ role: 'pasteAndMatchStyle' },
|
||||||
|
{ role: 'delete' },
|
||||||
|
{ role: 'selectAll' },
|
||||||
|
{ type: 'separator' },
|
||||||
|
{
|
||||||
|
label: 'Speech',
|
||||||
|
submenu: [{ role: 'startSpeaking' }, { role: 'stopSpeaking' }],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
label: 'Edit',
|
||||||
|
submenu: [
|
||||||
|
{
|
||||||
|
label: 'Rename project',
|
||||||
|
id: 'Edit.Rename project',
|
||||||
|
click: () => {
|
||||||
|
typeSafeWebContentsSend(mainWindow, 'menu-action-clicked', {
|
||||||
|
menuLabel: 'Edit.Rename project',
|
||||||
|
})
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Delete project',
|
||||||
|
id: 'Edit.Delete project',
|
||||||
|
click: () => {
|
||||||
|
typeSafeWebContentsSend(mainWindow, 'menu-action-clicked', {
|
||||||
|
menuLabel: 'Edit.Delete project',
|
||||||
|
})
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ type: 'separator' },
|
||||||
|
{
|
||||||
|
label: 'Change project directory',
|
||||||
|
id: 'Edit.Change project directory',
|
||||||
|
click: () => {
|
||||||
|
typeSafeWebContentsSend(mainWindow, 'menu-action-clicked', {
|
||||||
|
menuLabel: 'Edit.Change project directory',
|
||||||
|
})
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ type: 'separator' },
|
||||||
|
{ role: 'undo' },
|
||||||
|
{ role: 'redo' },
|
||||||
|
{ type: 'separator' },
|
||||||
|
{ role: 'cut' },
|
||||||
|
{ role: 'copy' },
|
||||||
|
{ role: 'paste' },
|
||||||
|
...extraBits,
|
||||||
|
],
|
||||||
|
}
|
||||||
|
}
|
100
src/menu/fileRole.ts
Normal file
100
src/menu/fileRole.ts
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
import { BrowserWindow } from 'electron'
|
||||||
|
import { typeSafeWebContentsSend } from './channels'
|
||||||
|
import { ZooMenuItemConstructorOptions } from './roles'
|
||||||
|
import os from 'node:os'
|
||||||
|
const isMac = os.platform() === 'darwin'
|
||||||
|
|
||||||
|
export const projectFileRole = (
|
||||||
|
mainWindow: BrowserWindow
|
||||||
|
): ZooMenuItemConstructorOptions => {
|
||||||
|
return {
|
||||||
|
label: 'File',
|
||||||
|
submenu: [
|
||||||
|
{
|
||||||
|
label: 'New project',
|
||||||
|
id: 'File.New project',
|
||||||
|
accelerator: 'CommandOrControl+N',
|
||||||
|
click: () => {
|
||||||
|
typeSafeWebContentsSend(mainWindow, 'menu-action-clicked', {
|
||||||
|
menuLabel: 'File.New project',
|
||||||
|
})
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Open project',
|
||||||
|
id: 'File.Open project',
|
||||||
|
accelerator: 'CommandOrControl+P',
|
||||||
|
click: () => {
|
||||||
|
typeSafeWebContentsSend(mainWindow, 'menu-action-clicked', {
|
||||||
|
menuLabel: 'File.Open project',
|
||||||
|
})
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// TODO https://www.electronjs.org/docs/latest/tutorial/recent-documents
|
||||||
|
// Appears to be only Windows and Mac OS specific. Linux does not have support
|
||||||
|
{ type: 'separator' },
|
||||||
|
{
|
||||||
|
label: 'Preferences',
|
||||||
|
submenu: [
|
||||||
|
{
|
||||||
|
label: 'User settings',
|
||||||
|
id: 'File.Preferences.User settings',
|
||||||
|
click: () => {
|
||||||
|
typeSafeWebContentsSend(mainWindow, 'menu-action-clicked', {
|
||||||
|
menuLabel: 'File.Preferences.User settings',
|
||||||
|
})
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Keybindings',
|
||||||
|
id: 'File.Preferences.Keybindings',
|
||||||
|
click: () => {
|
||||||
|
typeSafeWebContentsSend(mainWindow, 'menu-action-clicked', {
|
||||||
|
menuLabel: 'File.Preferences.Keybindings',
|
||||||
|
})
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'User default units',
|
||||||
|
id: 'File.Preferences.User default units',
|
||||||
|
click: () => {
|
||||||
|
typeSafeWebContentsSend(mainWindow, 'menu-action-clicked', {
|
||||||
|
menuLabel: 'File.Preferences.User default units',
|
||||||
|
})
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Theme',
|
||||||
|
id: 'File.Preferences.Theme',
|
||||||
|
click: () => {
|
||||||
|
typeSafeWebContentsSend(mainWindow, 'menu-action-clicked', {
|
||||||
|
menuLabel: 'File.Preferences.Theme',
|
||||||
|
})
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Theme color',
|
||||||
|
id: 'File.Preferences.Theme color',
|
||||||
|
click: () => {
|
||||||
|
typeSafeWebContentsSend(mainWindow, 'menu-action-clicked', {
|
||||||
|
menuLabel: 'File.Preferences.Theme color',
|
||||||
|
})
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{ type: 'separator' },
|
||||||
|
// Last in list
|
||||||
|
{
|
||||||
|
label: 'Sign out',
|
||||||
|
id: 'File.Sign out',
|
||||||
|
click: () => {
|
||||||
|
typeSafeWebContentsSend(mainWindow, 'menu-action-clicked', {
|
||||||
|
menuLabel: 'File.Sign out',
|
||||||
|
})
|
||||||
|
},
|
||||||
|
},
|
||||||
|
isMac ? { role: 'close' } : { role: 'quit' },
|
||||||
|
],
|
||||||
|
}
|
||||||
|
}
|
113
src/menu/helpRole.ts
Normal file
113
src/menu/helpRole.ts
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
import { shell, BrowserWindow } from 'electron'
|
||||||
|
import { ZooMenuItemConstructorOptions } from './roles'
|
||||||
|
import { reportRejection } from 'lib/trap'
|
||||||
|
import { typeSafeWebContentsSend } from './channels'
|
||||||
|
|
||||||
|
export const helpRole = (
|
||||||
|
mainWindow: BrowserWindow
|
||||||
|
): ZooMenuItemConstructorOptions => {
|
||||||
|
return {
|
||||||
|
label: 'Help',
|
||||||
|
submenu: [
|
||||||
|
{
|
||||||
|
id: 'Help.Show all commands',
|
||||||
|
label: 'Show all commands',
|
||||||
|
click: () => {
|
||||||
|
typeSafeWebContentsSend(mainWindow, 'menu-action-clicked', {
|
||||||
|
menuLabel: 'Help.Command Palette...',
|
||||||
|
})
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'KCL code samples',
|
||||||
|
id: 'Help.KCL code samples',
|
||||||
|
click: () => {
|
||||||
|
shell
|
||||||
|
.openExternal('https://zoo.dev/docs/kcl-samples')
|
||||||
|
.catch(reportRejection)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'KCL docs',
|
||||||
|
click: () => {
|
||||||
|
shell.openExternal('https://zoo.dev/docs/kcl').catch(reportRejection)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Get started with Text-to-CAD',
|
||||||
|
click: () => {
|
||||||
|
shell
|
||||||
|
.openExternal('https://text-to-cad.zoo.dev/dashboard')
|
||||||
|
.catch(reportRejection)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ type: 'separator' },
|
||||||
|
{
|
||||||
|
label: 'Ask the community discord',
|
||||||
|
click: () => {
|
||||||
|
shell
|
||||||
|
.openExternal('https://discord.gg/JQEpHR7Nt2')
|
||||||
|
.catch(reportRejection)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Ask the community discourse',
|
||||||
|
click: () => {
|
||||||
|
shell
|
||||||
|
.openExternal('https://community.zoo.dev/')
|
||||||
|
.catch(reportRejection)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ type: 'separator' },
|
||||||
|
{
|
||||||
|
label: 'Refresh and report a bug',
|
||||||
|
id: 'Help.Refresh and report a bug',
|
||||||
|
click: () => {
|
||||||
|
typeSafeWebContentsSend(mainWindow, 'menu-action-clicked', {
|
||||||
|
menuLabel: 'Help.Refresh and report a bug',
|
||||||
|
})
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Request a feature',
|
||||||
|
click: () => {
|
||||||
|
shell
|
||||||
|
.openExternal(
|
||||||
|
'https://github.com/KittyCAD/modeling-app/discussions'
|
||||||
|
)
|
||||||
|
.catch(reportRejection)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ type: 'separator' },
|
||||||
|
{
|
||||||
|
id: 'Help.Reset onboarding',
|
||||||
|
label: 'Reset onboarding',
|
||||||
|
click: () => {
|
||||||
|
typeSafeWebContentsSend(mainWindow, 'menu-action-clicked', {
|
||||||
|
menuLabel: 'Help.Reset onboarding',
|
||||||
|
})
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ type: 'separator' },
|
||||||
|
{ role: 'toggleDevTools' },
|
||||||
|
{ role: 'reload' },
|
||||||
|
{ role: 'forceReload' },
|
||||||
|
{ type: 'separator' },
|
||||||
|
{
|
||||||
|
label: 'Show release notes',
|
||||||
|
click: () => {
|
||||||
|
shell
|
||||||
|
.openExternal('https://github.com/KittyCAD/modeling-app/releases')
|
||||||
|
.catch(reportRejection)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ type: 'separator' },
|
||||||
|
{
|
||||||
|
label: 'Manage account',
|
||||||
|
click: () => {
|
||||||
|
shell.openExternal('https://zoo.dev/account').catch(reportRejection)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
}
|
61
src/menu/roles.ts
Normal file
61
src/menu/roles.ts
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
// Does not matter what labels belong to what type. I only split these into some internal types to easily parse
|
||||||
|
// what labels should belong to what grouping
|
||||||
|
import { Menu, MenuItemConstructorOptions } from 'electron'
|
||||||
|
|
||||||
|
type HeaderLabel =
|
||||||
|
| 'File'
|
||||||
|
| 'Edit'
|
||||||
|
| 'Options'
|
||||||
|
| 'Window'
|
||||||
|
| 'Utility'
|
||||||
|
| 'Help'
|
||||||
|
| 'View'
|
||||||
|
|
||||||
|
type FileRoleLabel =
|
||||||
|
| 'Open project'
|
||||||
|
| 'New project'
|
||||||
|
| 'Import file from URL'
|
||||||
|
| 'Preferences'
|
||||||
|
| 'User settings'
|
||||||
|
| 'Keybindings'
|
||||||
|
| 'Sign out'
|
||||||
|
| 'Theme'
|
||||||
|
| 'Theme color'
|
||||||
|
| 'User default units'
|
||||||
|
|
||||||
|
type EditRoleLabel =
|
||||||
|
| 'Rename project'
|
||||||
|
| 'Delete project'
|
||||||
|
| 'Change project directory'
|
||||||
|
| 'Speech'
|
||||||
|
|
||||||
|
type HelpRoleLabel =
|
||||||
|
| 'Refresh and report a bug'
|
||||||
|
| 'Request a feature'
|
||||||
|
| 'Ask the community discord'
|
||||||
|
| 'Ask the community discourse'
|
||||||
|
| 'KCL code samples'
|
||||||
|
| 'KCL docs'
|
||||||
|
| 'Reset onboarding'
|
||||||
|
| 'Show release notes'
|
||||||
|
| 'Manage account'
|
||||||
|
| 'Get started with Text-to-CAD'
|
||||||
|
| 'Show all commands'
|
||||||
|
|
||||||
|
type ViewRoleLabel = 'Command Palette...' | 'Appearance'
|
||||||
|
|
||||||
|
// Only export the union of all the internal types since they are all labels
|
||||||
|
// The internal types are only for readability within the file
|
||||||
|
export type ZooLabel =
|
||||||
|
| HeaderLabel
|
||||||
|
| FileRoleLabel
|
||||||
|
| EditRoleLabel
|
||||||
|
| HelpRoleLabel
|
||||||
|
| ViewRoleLabel
|
||||||
|
|
||||||
|
// Extend the interface with additional custom properties
|
||||||
|
export interface ZooMenuItemConstructorOptions
|
||||||
|
extends MenuItemConstructorOptions {
|
||||||
|
label?: ZooLabel
|
||||||
|
submenu?: ZooMenuItemConstructorOptions[] | Menu
|
||||||
|
}
|
47
src/menu/viewRole.ts
Normal file
47
src/menu/viewRole.ts
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
import { BrowserWindow } from 'electron'
|
||||||
|
import { ZooMenuItemConstructorOptions } from './roles'
|
||||||
|
import { typeSafeWebContentsSend } from './channels'
|
||||||
|
import os from 'node:os'
|
||||||
|
const isMac = os.platform() === 'darwin'
|
||||||
|
|
||||||
|
export const projectViewRole = (
|
||||||
|
mainWindow: BrowserWindow
|
||||||
|
): ZooMenuItemConstructorOptions => {
|
||||||
|
let extraBits: ZooMenuItemConstructorOptions[] = [{ role: 'close' }]
|
||||||
|
if (isMac) {
|
||||||
|
extraBits = [
|
||||||
|
{ type: 'separator' },
|
||||||
|
{ role: 'front' },
|
||||||
|
{ type: 'separator' },
|
||||||
|
{ role: 'window' },
|
||||||
|
]
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
label: 'View',
|
||||||
|
submenu: [
|
||||||
|
{
|
||||||
|
label: 'Command Palette...',
|
||||||
|
id: 'View.Command Palette...',
|
||||||
|
click: () => {
|
||||||
|
typeSafeWebContentsSend(mainWindow, 'menu-action-clicked', {
|
||||||
|
menuLabel: 'View.Command Palette...',
|
||||||
|
})
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Appearance',
|
||||||
|
submenu: [
|
||||||
|
{ role: 'togglefullscreen' },
|
||||||
|
{ type: 'separator' },
|
||||||
|
{ role: 'zoomIn' },
|
||||||
|
{ role: 'zoomOut' },
|
||||||
|
{ role: 'resetZoom' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{ type: 'separator' },
|
||||||
|
{ role: 'minimize' },
|
||||||
|
{ role: 'zoom' },
|
||||||
|
...extraBits,
|
||||||
|
],
|
||||||
|
}
|
||||||
|
}
|
@ -1,4 +1,4 @@
|
|||||||
import { ipcRenderer, contextBridge } from 'electron'
|
import { ipcRenderer, contextBridge, IpcRendererEvent } from 'electron'
|
||||||
import path from 'path'
|
import path from 'path'
|
||||||
import fs from 'node:fs/promises'
|
import fs from 'node:fs/promises'
|
||||||
import os from 'node:os'
|
import os from 'node:os'
|
||||||
@ -6,6 +6,13 @@ import fsSync from 'node:fs'
|
|||||||
import packageJson from '../package.json'
|
import packageJson from '../package.json'
|
||||||
import { MachinesListing } from 'components/MachineManagerProvider'
|
import { MachinesListing } from 'components/MachineManagerProvider'
|
||||||
import chokidar from 'chokidar'
|
import chokidar from 'chokidar'
|
||||||
|
import type { Channel } from './channels'
|
||||||
|
import type { WebContentSendPayload } from './menu/channels'
|
||||||
|
|
||||||
|
const typeSafeIpcRendererOn = (
|
||||||
|
channel: Channel,
|
||||||
|
listener: (event: IpcRendererEvent, ...args: any[]) => Promise<void> | any
|
||||||
|
) => ipcRenderer.on(channel, listener)
|
||||||
|
|
||||||
const resizeWindow = (width: number, height: number) =>
|
const resizeWindow = (width: number, height: number) =>
|
||||||
ipcRenderer.invoke('app.resizeWindow', [width, height])
|
ipcRenderer.invoke('app.resizeWindow', [width, height])
|
||||||
@ -163,6 +170,58 @@ const getArgvParsed = () => {
|
|||||||
return ipcRenderer.invoke('argv.parser')
|
return ipcRenderer.invoke('argv.parser')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Creating a menu will refresh the state of the menu
|
||||||
|
// Anything that was enabled will be reset to the hard coded state of the original menu
|
||||||
|
const createHomePageMenu = async (): Promise<any> => {
|
||||||
|
return ipcRenderer.invoke('create-menu', { page: 'project' })
|
||||||
|
}
|
||||||
|
|
||||||
|
// Creating a menu will refresh the state of the menu
|
||||||
|
// Anything that was enabled will be reset to the hard coded state of the original menu
|
||||||
|
const createModelingPageMenu = async (): Promise<any> => {
|
||||||
|
return ipcRenderer.invoke('create-menu', { page: 'modeling' })
|
||||||
|
}
|
||||||
|
|
||||||
|
// Creating a menu will refresh the state of the menu
|
||||||
|
// Anything that was enabled will be reset to the hard coded state of the original menu
|
||||||
|
const createFallbackMenu = async (): Promise<any> => {
|
||||||
|
return ipcRenderer.invoke('create-menu', { page: 'fallback' })
|
||||||
|
}
|
||||||
|
|
||||||
|
// Given the application menu, try to enable the menu
|
||||||
|
const enableMenu = async (menuId: string): Promise<any> => {
|
||||||
|
return ipcRenderer.invoke('enable-menu', {
|
||||||
|
menuId,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Given the application menu, try to disable the menu
|
||||||
|
const disableMenu = async (menuId: string): Promise<any> => {
|
||||||
|
return ipcRenderer.invoke('disable-menu', {
|
||||||
|
menuId,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gotcha: Even if the callback function is the same function in JS memory
|
||||||
|
* when passing it over the IPC layer it will not map to the same function.
|
||||||
|
* this means your .on and .off with the same callback function in memory will
|
||||||
|
* not be removed.
|
||||||
|
* To remove the listener call the return value of menuOn. It builds a closure
|
||||||
|
* of the subscription on the electron side and it will let you remove the listener correctly.
|
||||||
|
*/
|
||||||
|
const menuOn = (callback: (payload: WebContentSendPayload) => void) => {
|
||||||
|
// Build a new subscription function for the closure below
|
||||||
|
const subscription = (event: IpcRendererEvent, data: WebContentSendPayload) =>
|
||||||
|
callback(data)
|
||||||
|
typeSafeIpcRendererOn('menu-action-clicked', subscription)
|
||||||
|
|
||||||
|
// This is the only way to remove the event listener from the JS side
|
||||||
|
return () => {
|
||||||
|
ipcRenderer.removeListener('menu-action-clicked', subscription)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
contextBridge.exposeInMainWorld('electron', {
|
contextBridge.exposeInMainWorld('electron', {
|
||||||
startDeviceFlow,
|
startDeviceFlow,
|
||||||
loginWithDeviceFlow,
|
loginWithDeviceFlow,
|
||||||
@ -237,5 +296,11 @@ contextBridge.exposeInMainWorld('electron', {
|
|||||||
appCheckForUpdates,
|
appCheckForUpdates,
|
||||||
getArgvParsed,
|
getArgvParsed,
|
||||||
resizeWindow,
|
resizeWindow,
|
||||||
|
createHomePageMenu,
|
||||||
|
createModelingPageMenu,
|
||||||
|
createFallbackMenu,
|
||||||
|
enableMenu,
|
||||||
|
disableMenu,
|
||||||
|
menuOn,
|
||||||
canReadWriteDirectory,
|
canReadWriteDirectory,
|
||||||
})
|
})
|
||||||
|
@ -24,6 +24,9 @@ import { commandBarActor } from 'machines/commandBarMachine'
|
|||||||
import { useCreateFileLinkQuery } from 'hooks/useCreateFileLinkQueryWatcher'
|
import { useCreateFileLinkQuery } from 'hooks/useCreateFileLinkQueryWatcher'
|
||||||
import { useSettings } from 'machines/appMachine'
|
import { useSettings } from 'machines/appMachine'
|
||||||
import { reportRejection } from 'lib/trap'
|
import { reportRejection } from 'lib/trap'
|
||||||
|
import { authActor } from 'machines/appMachine'
|
||||||
|
import type { WebContentSendPayload } from '../menu/channels'
|
||||||
|
import { useMenuListener } from 'hooks/useMenu'
|
||||||
|
|
||||||
// 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.
|
||||||
@ -37,6 +40,13 @@ const Home = () => {
|
|||||||
error: undefined,
|
error: undefined,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Only create the native file menus on desktop
|
||||||
|
useEffect(() => {
|
||||||
|
if (isDesktop()) {
|
||||||
|
window.electron.createHomePageMenu().catch(reportRejection)
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
// Keep a lookout for a URL query string that invokes the 'import file from URL' command
|
// Keep a lookout for a URL query string that invokes the 'import file from URL' command
|
||||||
useCreateFileLinkQuery((argDefaultValues) => {
|
useCreateFileLinkQuery((argDefaultValues) => {
|
||||||
commandBarActor.send({
|
commandBarActor.send({
|
||||||
@ -52,6 +62,80 @@ const Home = () => {
|
|||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const settings = useSettings()
|
const settings = useSettings()
|
||||||
|
|
||||||
|
// Menu listeners
|
||||||
|
const cb = (data: WebContentSendPayload) => {
|
||||||
|
if (data.menuLabel === 'File.New project') {
|
||||||
|
commandBarActor.send({
|
||||||
|
type: 'Find and select command',
|
||||||
|
data: {
|
||||||
|
groupId: 'projects',
|
||||||
|
name: 'Create project',
|
||||||
|
argDefaultValues: {
|
||||||
|
name: settings.projects.defaultProjectName.current,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
} else if (data.menuLabel === 'File.Open project') {
|
||||||
|
commandBarActor.send({
|
||||||
|
type: 'Find and select command',
|
||||||
|
data: {
|
||||||
|
groupId: 'projects',
|
||||||
|
name: 'Open project',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
} else if (data.menuLabel === 'Edit.Rename project') {
|
||||||
|
commandBarActor.send({
|
||||||
|
type: 'Find and select command',
|
||||||
|
data: {
|
||||||
|
groupId: 'projects',
|
||||||
|
name: 'Rename project',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
} else if (data.menuLabel === 'Edit.Delete project') {
|
||||||
|
commandBarActor.send({
|
||||||
|
type: 'Find and select command',
|
||||||
|
data: {
|
||||||
|
groupId: 'projects',
|
||||||
|
name: 'Delete project',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
} else if (data.menuLabel === 'File.Import file from URL') {
|
||||||
|
commandBarActor.send({
|
||||||
|
type: 'Find and select command',
|
||||||
|
data: {
|
||||||
|
groupId: 'projects',
|
||||||
|
name: 'Import file from URL',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
} else if (data.menuLabel === 'File.Preferences.User settings') {
|
||||||
|
navigate(PATHS.HOME + PATHS.SETTINGS)
|
||||||
|
} else if (data.menuLabel === 'File.Preferences.Keybindings') {
|
||||||
|
navigate(PATHS.HOME + PATHS.SETTINGS_KEYBINDINGS)
|
||||||
|
} else if (data.menuLabel === 'File.Preferences.User default units') {
|
||||||
|
navigate(PATHS.HOME + PATHS.SETTINGS_USER + '#defaultUnit')
|
||||||
|
} else if (data.menuLabel === 'Edit.Change project directory') {
|
||||||
|
navigate(PATHS.HOME + PATHS.SETTINGS_USER + '#projectDirectory')
|
||||||
|
} else if (data.menuLabel === 'File.Sign out') {
|
||||||
|
authActor.send({ type: 'Log out' })
|
||||||
|
} else if (
|
||||||
|
data.menuLabel === 'View.Command Palette...' ||
|
||||||
|
data.menuLabel === 'Help.Command Palette...'
|
||||||
|
) {
|
||||||
|
commandBarActor.send({ type: 'Open' })
|
||||||
|
} else if (data.menuLabel === 'File.Preferences.Theme') {
|
||||||
|
commandBarActor.send({
|
||||||
|
type: 'Find and select command',
|
||||||
|
data: {
|
||||||
|
groupId: 'settings',
|
||||||
|
name: 'app.theme',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
} else if (data.menuLabel === 'File.Preferences.Theme color') {
|
||||||
|
navigate(PATHS.HOME + PATHS.SETTINGS_USER + '#themeColor')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
useMenuListener(cb)
|
||||||
|
|
||||||
// Cancel all KCL executions while on the home page
|
// Cancel all KCL executions while on the home page
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
markOnce('code/didLoadHome')
|
markOnce('code/didLoadHome')
|
||||||
|
@ -98,7 +98,10 @@ export const Settings = () => {
|
|||||||
leaveFrom="opacity-100 scale-100"
|
leaveFrom="opacity-100 scale-100"
|
||||||
leaveTo="opacity-0 scale-95"
|
leaveTo="opacity-0 scale-95"
|
||||||
>
|
>
|
||||||
<Dialog.Panel className="rounded relative mx-auto bg-chalkboard-10 dark:bg-chalkboard-100 border dark:border-chalkboard-70 max-w-3xl w-full max-h-[66vh] shadow-lg flex flex-col gap-8">
|
<Dialog.Panel
|
||||||
|
data-testid="settings-dialog-panel"
|
||||||
|
className="rounded relative mx-auto bg-chalkboard-10 dark:bg-chalkboard-100 border dark:border-chalkboard-70 max-w-3xl w-full max-h-[66vh] shadow-lg flex flex-col gap-8"
|
||||||
|
>
|
||||||
<div className="p-5 pb-0 flex justify-between items-center">
|
<div className="p-5 pb-0 flex justify-between items-center">
|
||||||
<h1 className="text-2xl font-bold">Settings</h1>
|
<h1 className="text-2xl font-bold">Settings</h1>
|
||||||
<div className="flex gap-4 items-start">
|
<div className="flex gap-4 items-start">
|
||||||
|
@ -21,6 +21,14 @@ const subtleBorder =
|
|||||||
const cardArea = `${subtleBorder} rounded-lg px-6 py-3 text-chalkboard-70 dark:text-chalkboard-30`
|
const cardArea = `${subtleBorder} rounded-lg px-6 py-3 text-chalkboard-70 dark:text-chalkboard-30`
|
||||||
|
|
||||||
const SignIn = () => {
|
const SignIn = () => {
|
||||||
|
// Only create the native file menus on desktop
|
||||||
|
if (isDesktop()) {
|
||||||
|
window.electron.createFallbackMenu().catch(reportRejection)
|
||||||
|
// Disable these since they cannot be accessed within the sign in page.
|
||||||
|
window.electron.disableMenu('Help.Reset onboarding').catch(reportRejection)
|
||||||
|
window.electron.disableMenu('Help.Show all commands').catch(reportRejection)
|
||||||
|
}
|
||||||
|
|
||||||
const [userCode, setUserCode] = useState('')
|
const [userCode, setUserCode] = useState('')
|
||||||
const {
|
const {
|
||||||
app: { theme },
|
app: { theme },
|
||||||
|
Reference in New Issue
Block a user