diff --git a/app/ide-desktop/eslint.config.js b/app/ide-desktop/eslint.config.js index 3296e9c64949..bb1b07453527 100644 --- a/app/ide-desktop/eslint.config.js +++ b/app/ide-desktop/eslint.config.js @@ -240,8 +240,8 @@ const RESTRICTED_SYNTAXES = [ export default [ eslintJs.configs.recommended, { - // Playwright build cache. - ignores: ['**/.cache/**', '**/playwright-report', 'dist'], + // Playwright build cache and Vite build directory. + ignores: ['**/.cache/**', '**/playwright-report', '**/dist'], }, { settings: { @@ -334,7 +334,7 @@ export default [ format: ['camelCase', 'PascalCase'], }, { - selector: ['parameter', 'property', 'method'], + selector: ['parameter', 'method'], format: ['camelCase'], }, { @@ -343,6 +343,14 @@ export default [ format: ['camelCase'], leadingUnderscore: 'require', }, + { + selector: ['property'], + format: ['camelCase'], + filter: { + regex: '^(?:data-testid)$', + match: false, + }, + }, ], '@typescript-eslint/no-confusing-void-expression': 'error', '@typescript-eslint/no-empty-interface': 'off', diff --git a/app/ide-desktop/lib/client/src/index.ts b/app/ide-desktop/lib/client/src/index.ts index d5a0e68499a4..14c02dd19791 100644 --- a/app/ide-desktop/lib/client/src/index.ts +++ b/app/ide-desktop/lib/client/src/index.ts @@ -332,6 +332,32 @@ class App { } const window = new electron.BrowserWindow(windowPreferences) window.setMenuBarVisibility(false) + const oldMenu = electron.Menu.getApplicationMenu() + if (oldMenu != null) { + const items = oldMenu.items.map(item => { + if (item.role !== 'help') { + return item + } else { + // `click` is a property that is intentionally removed from this + // destructured object, in order to satisfy TypeScript. + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { click, ...passthrough } = item + return new electron.MenuItem({ + ...passthrough, + submenu: electron.Menu.buildFromTemplate([ + new electron.MenuItem({ + label: `About ${common.PRODUCT_NAME}`, + click: () => { + window.webContents.send(ipc.Channel.showAboutModal) + }, + }), + ]), + }) + } + }) + const newMenu = electron.Menu.buildFromTemplate(items) + electron.Menu.setApplicationMenu(newMenu) + } if (this.args.groups.debug.options.devTools.value) { window.webContents.openDevTools() diff --git a/app/ide-desktop/lib/client/src/ipc.ts b/app/ide-desktop/lib/client/src/ipc.ts index 1fcd1584434a..d799783a9b69 100644 --- a/app/ide-desktop/lib/client/src/ipc.ts +++ b/app/ide-desktop/lib/client/src/ipc.ts @@ -27,4 +27,5 @@ export enum Channel { goForward = 'go-forward', /** Channel for selecting files and directories using the system file browser. */ openFileBrowser = 'open-file-browser', + showAboutModal = 'show-about-modal', } diff --git a/app/ide-desktop/lib/client/src/preload.ts b/app/ide-desktop/lib/client/src/preload.ts index 7b4f01b2fe51..675a4af965fa 100644 --- a/app/ide-desktop/lib/client/src/preload.ts +++ b/app/ide-desktop/lib/client/src/preload.ts @@ -5,6 +5,7 @@ import * as electron from 'electron' +import * as debug from 'debug' import * as ipc from 'ipc' // ================= @@ -23,6 +24,10 @@ const FILE_BROWSER_API_KEY = 'fileBrowserApi' const NAVIGATION_API_KEY = 'navigationApi' +const MENU_API_KEY = 'menuApi' + +const VERSION_INFO_KEY = 'versionInfo' + // ============================= // === importProjectFromPath === // ============================= @@ -172,3 +177,27 @@ const FILE_BROWSER_API = { electron.ipcRenderer.invoke(ipc.Channel.openFileBrowser, kind), } electron.contextBridge.exposeInMainWorld(FILE_BROWSER_API_KEY, FILE_BROWSER_API) + +// ==================== +// === Version info === +// ==================== + +electron.contextBridge.exposeInMainWorld(VERSION_INFO_KEY, debug.VERSION_INFO) + +// ================ +// === Menu API === +// ================ + +let showAboutModalHandler: (() => void) | null = null + +electron.ipcRenderer.on(ipc.Channel.showAboutModal, () => { + showAboutModalHandler?.() +}) + +const MENU_API = { + setShowAboutModalHandler: (callback: () => void) => { + showAboutModalHandler = callback + }, +} + +electron.contextBridge.exposeInMainWorld(MENU_API_KEY, MENU_API) diff --git a/app/ide-desktop/lib/common/src/appConfig.js b/app/ide-desktop/lib/common/src/appConfig.js index 679c936ca6b1..0b194464470e 100644 --- a/app/ide-desktop/lib/common/src/appConfig.js +++ b/app/ide-desktop/lib/common/src/appConfig.js @@ -3,6 +3,8 @@ import * as fs from 'node:fs/promises' import * as path from 'node:path' import * as url from 'node:url' +import BUILD_INFO from '../../../../../build.json' assert { type: 'json' } + // =============================== // === readEnvironmentFromFile === // =============================== @@ -11,7 +13,9 @@ import * as url from 'node:url' * environment variable. Reads from `.env` if the variable is `production`, blank or absent. * DOES NOT override existing environment variables if the variable is absent. */ export async function readEnvironmentFromFile() { + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition const environment = process.env.ENSO_CLOUD_ENVIRONMENT ?? null + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition const isProduction = environment == null || environment === '' || environment === 'production' const fileName = isProduction ? '.env' : `.${environment}.env` const filePath = path.join(url.fileURLToPath(new URL('../../..', import.meta.url)), fileName) @@ -38,6 +42,7 @@ export async function readEnvironmentFromFile() { if (isProduction) { entries = entries.filter(kv => { const [k] = kv + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition return process.env[k] == null }) } @@ -45,6 +50,10 @@ export async function readEnvironmentFromFile() { if (!isProduction || entries.length > 0) { Object.assign(process.env, variables) } + // @ts-expect-error This is the only file where `process.env` should be written to. + process.env.ENSO_CLOUD_DASHBOARD_VERSION ??= BUILD_INFO.version + // @ts-expect-error This is the only file where `process.env` should be written to. + process.env.ENSO_CLOUD_DASHBOARD_COMMIT_HASH ??= BUILD_INFO.commit } catch (error) { if (missingKeys.length !== 0) { console.warn('Could not load `.env` file; disabling cloud backend.') @@ -100,6 +109,12 @@ export function getDefines(serverPort = 8080) { 'process.env.ENSO_CLOUD_GOOGLE_ANALYTICS_TAG': stringify( process.env.ENSO_CLOUD_GOOGLE_ANALYTICS_TAG ), + 'process.env.ENSO_CLOUD_DASHBOARD_VERSION': stringify( + process.env.ENSO_CLOUD_DASHBOARD_VERSION + ), + 'process.env.ENSO_CLOUD_DASHBOARD_COMMIT_HASH': stringify( + process.env.ENSO_CLOUD_DASHBOARD_COMMIT_HASH + ), /* eslint-enable @typescript-eslint/naming-convention */ } } @@ -119,12 +134,15 @@ const DUMMY_DEFINES = { 'process.env.ENSO_CLOUD_COGNITO_USER_POOL_WEB_CLIENT_ID': '', 'process.env.ENSO_CLOUD_COGNITO_DOMAIN': '', 'process.env.ENSO_CLOUD_COGNITO_REGION': '', + 'process.env.ENSO_CLOUD_DASHBOARD_VERSION': '0.0.1-testing', + 'process.env.ENSO_CLOUD_DASHBOARD_COMMIT_HASH': 'abcdef0', /* eslint-enable @typescript-eslint/naming-convention */ } /** Load test environment variables, useful for when the Cloud backend is mocked or unnecessary. */ export function loadTestEnvironmentVariables() { for (const [k, v] of Object.entries(DUMMY_DEFINES)) { + // @ts-expect-error This is the only file where `process.env` should be written to. process.env[k.replace(/^process[.]env[.]/, '')] = v } } diff --git a/app/ide-desktop/lib/common/src/buildUtils.js b/app/ide-desktop/lib/common/src/buildUtils.js index 8f11a12e7e31..7657aabb1798 100644 --- a/app/ide-desktop/lib/common/src/buildUtils.js +++ b/app/ide-desktop/lib/common/src/buildUtils.js @@ -20,6 +20,7 @@ export const INDENT_SIZE = 4 * @throws {Error} If the environment variable is not set. */ export function requireEnv(name) { const value = process.env[name] + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition if (value == null) { throw new Error(`Could not find the environment variable '${name}'.`) } else { diff --git a/app/ide-desktop/lib/common/src/detect.ts b/app/ide-desktop/lib/common/src/detect.ts index a2f97df1459e..f626f0684ca7 100644 --- a/app/ide-desktop/lib/common/src/detect.ts +++ b/app/ide-desktop/lib/common/src/detect.ts @@ -5,6 +5,7 @@ // =================== /** Return whether the current build is in development mode */ +// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition export const IS_DEV_MODE = process.env.NODE_ENV === 'development' // ================ diff --git a/app/ide-desktop/lib/common/tsconfig.json b/app/ide-desktop/lib/common/tsconfig.json index 0a921bf939ad..3a319d3b29f6 100644 --- a/app/ide-desktop/lib/common/tsconfig.json +++ b/app/ide-desktop/lib/common/tsconfig.json @@ -5,5 +5,5 @@ "checkJs": false, "skipLibCheck": false }, - "include": ["./src/"] + "include": ["./src/", "../types/"] } diff --git a/app/ide-desktop/lib/dashboard/src/App.tsx b/app/ide-desktop/lib/dashboard/src/App.tsx index 5b0c5d93325e..03cbe4b32b79 100644 --- a/app/ide-desktop/lib/dashboard/src/App.tsx +++ b/app/ide-desktop/lib/dashboard/src/App.tsx @@ -52,9 +52,10 @@ import InputBindingsProvider from '#/providers/InputBindingsProvider' import LocalStorageProvider, * as localStorageProvider from '#/providers/LocalStorageProvider' import LoggerProvider from '#/providers/LoggerProvider' import type * as loggerProvider from '#/providers/LoggerProvider' -import ModalProvider from '#/providers/ModalProvider' +import ModalProvider, * as modalProvider from '#/providers/ModalProvider' import * as navigator2DProvider from '#/providers/Navigator2DProvider' import SessionProvider from '#/providers/SessionProvider' +import SupportsLocalBackendProvider from '#/providers/SupportsLocalBackendProvider' import ConfirmRegistration from '#/pages/authentication/ConfirmRegistration' import EnterOfflineMode from '#/pages/authentication/EnterOfflineMode' @@ -74,6 +75,7 @@ import * as errorBoundary from '#/components/ErrorBoundary' import * as loader from '#/components/Loader' import * as rootComponent from '#/components/Root' +import AboutModal from '#/modals/AboutModal' import * as setOrganizationNameModal from '#/modals/SetOrganizationNameModal' import type Backend from '#/services/Backend' @@ -197,7 +199,9 @@ export default function App(props: AppProps) { /> - + + + @@ -226,6 +230,7 @@ function AppRouter(props: AppRouterProps) { // eslint-disable-next-line no-restricted-properties const navigate = router.useNavigate() const { localStorage } = localStorageProvider.useLocalStorage() + const { setModal } = modalProvider.useSetModal() const navigator2D = navigator2DProvider.useNavigator2D() if (detect.IS_DEV_MODE) { // @ts-expect-error This is used exclusively for debugging. @@ -320,6 +325,14 @@ function AppRouter(props: AppRouterProps) { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion null! + React.useEffect(() => { + if ('menuApi' in window) { + window.menuApi.setShowAboutModalHandler(() => { + setModal() + }) + } + }, [/* should never change */ setModal]) + React.useEffect(() => { const onKeyDown = navigator2D.onKeyDown.bind(navigator2D) document.addEventListener('keydown', onKeyDown) @@ -438,8 +451,12 @@ function AppRouter(props: AppRouterProps) { ) let result = routes + result = ( + + {result} + + ) result = {result} - result = {result} result = ( > = { + modal: '', popover: '', fullscreen: 'p-4', -} satisfies Record +} -const DIALOG_CLASSES_BY_TYPE = { +const DIALOG_CLASSES_BY_TYPE: Readonly> = { modal: 'w-full max-w-md min-h-[100px] max-h-[90vh]', popover: 'rounded-lg', fullscreen: 'w-full h-full max-w-full max-h-full bg-clip-border', -} satisfies Record +} + +// ============== +// === Dialog === +// ============== -/** - * A dialog is an overlay shown above other content in an application. - * Can be used to display alerts, confirmations, or other content. - */ +/** A dialog is an overlay shown above other content in an application. + * Can be used to display alerts, confirmations, or other content. */ export function Dialog(props: types.DialogProps) { const { children, title, type = 'modal', - isDismissible = true, + isDismissable = true, isKeyboardDismissDisabled = false, hideCloseButton = false, className, @@ -50,21 +52,36 @@ export function Dialog(props: types.DialogProps) { modalProps, ...ariaDialogProps } = props + const cleanupRef = React.useRef(() => {}) const root = portal.useStrictPortalContext() return ( { + cleanupRef.current() + if (element == null) { + cleanupRef.current = () => {} + } else { + const onClick = (event: Event) => { + event.stopPropagation() + } + element.addEventListener('click', onClick) + cleanupRef.current = () => { + element.removeEventListener('click', onClick) + } + } + }} > {opts => ( <> diff --git a/app/ide-desktop/lib/dashboard/src/components/AriaComponents/Dialog/types.ts b/app/ide-desktop/lib/dashboard/src/components/AriaComponents/Dialog/types.ts index 9bd9cb9c542d..7b6f2693832e 100644 --- a/app/ide-desktop/lib/dashboard/src/components/AriaComponents/Dialog/types.ts +++ b/app/ide-desktop/lib/dashboard/src/components/AriaComponents/Dialog/types.ts @@ -10,7 +10,7 @@ export interface DialogProps extends aria.DialogProps { * @default 'modal' */ readonly type?: DialogType readonly title?: string - readonly isDismissible?: boolean + readonly isDismissable?: boolean readonly hideCloseButton?: boolean readonly onOpenChange?: (isOpen: boolean) => void readonly isKeyboardDismissDisabled?: boolean diff --git a/app/ide-desktop/lib/dashboard/src/components/EditableSpan.tsx b/app/ide-desktop/lib/dashboard/src/components/EditableSpan.tsx index 54dfd321dd14..c6a5019aaeb4 100644 --- a/app/ide-desktop/lib/dashboard/src/components/EditableSpan.tsx +++ b/app/ide-desktop/lib/dashboard/src/components/EditableSpan.tsx @@ -23,8 +23,6 @@ import * as sanitizedEventTargets from '#/utilities/sanitizedEventTargets' /** Props for an {@link EditableSpan}. */ export interface EditableSpanProps { - // This matches the capitalization of `data-` attributes in React. - // eslint-disable-next-line @typescript-eslint/naming-convention readonly 'data-testid'?: string readonly className?: string readonly editable?: boolean diff --git a/app/ide-desktop/lib/dashboard/src/components/MenuEntry.tsx b/app/ide-desktop/lib/dashboard/src/components/MenuEntry.tsx index 5755a1e7a811..112848c724f5 100644 --- a/app/ide-desktop/lib/dashboard/src/components/MenuEntry.tsx +++ b/app/ide-desktop/lib/dashboard/src/components/MenuEntry.tsx @@ -58,6 +58,7 @@ const ACTION_TO_TEXT_ID: Readonly {innerProps => (
{ + readonly hideInfoBar?: true +} + +/** A page. */ +export default function Page(props: PageProps) { + const { hideInfoBar = false, children } = props + const [isHelpChatOpen, setIsHelpChatOpen] = React.useState(false) + const { unsetModal } = modalProvider.useSetModal() + const session = authProvider.useUserSession() + + const doCloseChat = () => { + setIsHelpChatOpen(false) + } + + React.useEffect(() => { + const onClick = () => { + if (getSelection()?.type !== 'Range') { + unsetModal() + } + } + document.addEventListener('click', onClick) + return () => { + document.removeEventListener('click', onClick) + } + }, [/* should never change */ unsetModal]) + + return ( + <> + {children} + {!hideInfoBar && ( +
+ +
+ )} + {/* `session.accessToken` MUST be present in order for the `Chat` component to work. */} + {!hideInfoBar && session?.accessToken != null && process.env.ENSO_CLOUD_CHAT_URL != null ? ( + + ) : ( + + )} + +
+ +
+
+ + ) +} diff --git a/app/ide-desktop/lib/dashboard/src/components/Root.tsx b/app/ide-desktop/lib/dashboard/src/components/Root.tsx index 528eb0ad2627..c1991921fc37 100644 --- a/app/ide-desktop/lib/dashboard/src/components/Root.tsx +++ b/app/ide-desktop/lib/dashboard/src/components/Root.tsx @@ -4,6 +4,10 @@ import * as React from 'react' import * as aria from '#/components/aria' import * as portal from '#/components/Portal' +// ============ +// === Root === +// ============ + /** Props for {@link Root}. */ export interface RootProps extends React.PropsWithChildren { readonly rootRef: React.RefObject diff --git a/app/ide-desktop/lib/dashboard/src/components/dashboard/Label.tsx b/app/ide-desktop/lib/dashboard/src/components/dashboard/Label.tsx index 9ec6bdf53b57..bf4264e4f18f 100644 --- a/app/ide-desktop/lib/dashboard/src/components/dashboard/Label.tsx +++ b/app/ide-desktop/lib/dashboard/src/components/dashboard/Label.tsx @@ -16,8 +16,6 @@ import * as backend from '#/services/Backend' /** Props for a {@link Label}. */ interface InternalLabelProps extends Readonly { - // This matches the capitalization of `data-` attributes in React. - // eslint-disable-next-line @typescript-eslint/naming-convention readonly 'data-testid'?: string /** When true, the button is not faded out even when not hovered. */ readonly active?: boolean diff --git a/app/ide-desktop/lib/dashboard/src/configurations/inputBindings.ts b/app/ide-desktop/lib/dashboard/src/configurations/inputBindings.ts index 3ed743a344fe..b2e0e8fa977d 100644 --- a/app/ide-desktop/lib/dashboard/src/configurations/inputBindings.ts +++ b/app/ide-desktop/lib/dashboard/src/configurations/inputBindings.ts @@ -14,6 +14,7 @@ import CopyIcon from 'enso-assets/copy.svg' import DataDownloadIcon from 'enso-assets/data_download.svg' import DataUploadIcon from 'enso-assets/data_upload.svg' import DuplicateIcon from 'enso-assets/duplicate.svg' +import LogoIcon from 'enso-assets/enso_logo.svg' import OpenIcon from 'enso-assets/open.svg' import PasteIcon from 'enso-assets/paste.svg' import PenIcon from 'enso-assets/pen.svg' @@ -120,4 +121,10 @@ export const BINDINGS = inputBindings.defineBindings({ rebindable: true, icon: ArrowRightIcon, }, + aboutThisApp: { + name: 'About Enso', + bindings: ['Mod+/'], + rebindable: true, + icon: LogoIcon, + }, }) diff --git a/app/ide-desktop/lib/dashboard/src/layouts/ChatPlaceholder.tsx b/app/ide-desktop/lib/dashboard/src/layouts/ChatPlaceholder.tsx index b9ef552ea7cf..d4598e192f97 100644 --- a/app/ide-desktop/lib/dashboard/src/layouts/ChatPlaceholder.tsx +++ b/app/ide-desktop/lib/dashboard/src/layouts/ChatPlaceholder.tsx @@ -18,14 +18,16 @@ import UnstyledButton from '#/components/UnstyledButton' /** Props for a {@link ChatPlaceholder}. */ export interface ChatPlaceholderProps { - /** This should only be false when the panel is closing. */ + /** This should only be `true` when in the auth flow. */ + readonly hideLoginButtons?: true + /** This should only be `false` when the panel is closing. */ readonly isOpen: boolean readonly doClose: () => void } /** A placeholder component replacing `Chat` when a user is not logged in. */ export default function ChatPlaceholder(props: ChatPlaceholderProps) { - const { isOpen, doClose } = props + const { hideLoginButtons = false, isOpen, doClose } = props const { getText } = textProvider.useText() const logger = loggerProvider.useLogger() const navigate = navigateHooks.useNavigate() @@ -51,22 +53,26 @@ export default function ChatPlaceholder(props: ChatPlaceholderProps) {
{getText('placeholderChatPrompt')}
- { - navigate(appUtils.LOGIN_PATH) - }} - > - {getText('login')} - - { - navigate(appUtils.REGISTRATION_PATH) - }} - > - {getText('register')} - + {!hideLoginButtons && ( + { + navigate(appUtils.LOGIN_PATH) + }} + > + {getText('login')} + + )} + {!hideLoginButtons && ( + { + navigate(appUtils.REGISTRATION_PATH) + }} + > + {getText('register')} + + )}
, diff --git a/app/ide-desktop/lib/dashboard/src/layouts/InfoBar.tsx b/app/ide-desktop/lib/dashboard/src/layouts/InfoBar.tsx new file mode 100644 index 000000000000..d56175b81314 --- /dev/null +++ b/app/ide-desktop/lib/dashboard/src/layouts/InfoBar.tsx @@ -0,0 +1,73 @@ +/** @file A toolbar containing chat and the user menu. */ +import * as React from 'react' + +import ChatIcon from 'enso-assets/chat.svg' +import LogoIcon from 'enso-assets/enso_logo.svg' + +import * as modalProvider from '#/providers/ModalProvider' +import * as textProvider from '#/providers/TextProvider' + +import InfoMenu from '#/layouts/InfoMenu' + +import Button from '#/components/styled/Button' +import FocusArea from '#/components/styled/FocusArea' +import SvgMask from '#/components/SvgMask' +import UnstyledButton from '#/components/UnstyledButton' + +// =============== +// === InfoBar === +// =============== + +/** Props for a {@link InfoBar}. */ +export interface InfoBarProps { + readonly isHelpChatOpen: boolean + readonly setIsHelpChatOpen: (isHelpChatOpen: boolean) => void +} + +/** A toolbar containing chat and the user menu. */ +export default function InfoBar(props: InfoBarProps) { + const { isHelpChatOpen, setIsHelpChatOpen } = props + const { updateModal } = modalProvider.useSetModal() + const { getText } = textProvider.useText() + + return ( + + {innerProps => ( +
+ {/* FIXME [sb]: https://github.com/enso-org/cloud-v2/issues/1227 + * Make help chat work even when signed out. + * Note that the original class for the `div` above is `pr-profile-picture px-icons-x`. */} + {/* eslint-disable-next-line @typescript-eslint/no-unnecessary-condition */} + {false && ( +
+ )} +
+ ) +} diff --git a/app/ide-desktop/lib/dashboard/src/layouts/InfoMenu.tsx b/app/ide-desktop/lib/dashboard/src/layouts/InfoMenu.tsx new file mode 100644 index 000000000000..d1d81e96fd4e --- /dev/null +++ b/app/ide-desktop/lib/dashboard/src/layouts/InfoMenu.tsx @@ -0,0 +1,79 @@ +/** @file A menu containing info about the app. */ +import * as React from 'react' + +import LogoIcon from 'enso-assets/enso_logo.svg' +import * as common from 'enso-common' + +import * as modalProvider from '#/providers/ModalProvider' +import * as textProvider from '#/providers/TextProvider' + +import * as aria from '#/components/aria' +import MenuEntry from '#/components/MenuEntry' +import Modal from '#/components/Modal' +import FocusArea from '#/components/styled/FocusArea' + +import AboutModal from '#/modals/AboutModal' + +// ================ +// === InfoMenu === +// ================ + +/** Props for an {@link InfoMenu}. */ +export interface InfoMenuProps { + readonly hidden?: boolean +} + +/** A menu containing info about the app. */ +export default function InfoMenu(props: InfoMenuProps) { + const { hidden = false } = props + const { setModal } = modalProvider.useSetModal() + const { getText } = textProvider.useText() + const [initialized, setInitialized] = React.useState(false) + + React.useLayoutEffect(() => { + // Change the CSS from the initial state to the final state after the first render. + // This ensures that the CSS transition triggers. + setInitialized(true) + }, []) + + return ( + + ) +} diff --git a/app/ide-desktop/lib/dashboard/src/layouts/Settings/UserGroupRow.tsx b/app/ide-desktop/lib/dashboard/src/layouts/Settings/UserGroupRow.tsx index 98045077eb06..7d6857767519 100644 --- a/app/ide-desktop/lib/dashboard/src/layouts/Settings/UserGroupRow.tsx +++ b/app/ide-desktop/lib/dashboard/src/layouts/Settings/UserGroupRow.tsx @@ -63,7 +63,7 @@ export default function UserGroupRow(props: UserGroupRowProps) { ref={contextMenuRef} > - + {needsTooltip && {userGroup.groupName}} - + - +
{needsTooltip && {user.name}} -
+
)} - + {needsTooltip && {user.name}} - + {user.email} diff --git a/app/ide-desktop/lib/dashboard/src/layouts/TopBar.tsx b/app/ide-desktop/lib/dashboard/src/layouts/TopBar.tsx index 06d1285a3385..470ce35b2f20 100644 --- a/app/ide-desktop/lib/dashboard/src/layouts/TopBar.tsx +++ b/app/ide-desktop/lib/dashboard/src/layouts/TopBar.tsx @@ -85,7 +85,6 @@ export default function TopBar(props: TopBarProps) { {supportsCloudBackend && ( )} void readonly setIsHelpChatOpen: (isHelpChatOpen: boolean) => void @@ -46,7 +45,7 @@ export interface UserBarProps { /** A toolbar containing chat and the user menu. */ export default function UserBar(props: UserBarProps) { const { invisible = false, page, setPage, setIsHelpChatOpen } = props - const { supportsLocalBackend, projectAsset, setProjectAsset, doRemoveSelf, onSignOut } = props + const { projectAsset, setProjectAsset, doRemoveSelf, onSignOut } = props const { type: sessionType, user } = authProvider.useNonPartialUserSession() const { setModal, updateModal } = modalProvider.useSetModal() const { backend } = backendProvider.useBackend() @@ -130,11 +129,7 @@ export default function UserBar(props: UserBarProps) { onPress={() => { updateModal(oldModal => oldModal?.type === UserMenu ? null : ( - + ) ) }} @@ -149,12 +144,7 @@ export default function UserBar(props: UserBarProps) {
{/* Required for shortcuts to work. */}
-
)} diff --git a/app/ide-desktop/lib/dashboard/src/layouts/UserMenu.tsx b/app/ide-desktop/lib/dashboard/src/layouts/UserMenu.tsx index bc3434aa75a5..c3d13ad13220 100644 --- a/app/ide-desktop/lib/dashboard/src/layouts/UserMenu.tsx +++ b/app/ide-desktop/lib/dashboard/src/layouts/UserMenu.tsx @@ -10,6 +10,7 @@ import * as toastAndLogHooks from '#/hooks/toastAndLogHooks' import * as authProvider from '#/providers/AuthProvider' import * as modalProvider from '#/providers/ModalProvider' +import * as supportsLocalBackendProvider from '#/providers/SupportsLocalBackendProvider' import * as textProvider from '#/providers/TextProvider' import * as pageSwitcher from '#/layouts/PageSwitcher' @@ -19,6 +20,8 @@ import MenuEntry from '#/components/MenuEntry' import Modal from '#/components/Modal' import FocusArea from '#/components/styled/FocusArea' +import AboutModal from '#/modals/AboutModal' + import * as download from '#/utilities/download' import * as github from '#/utilities/github' @@ -31,30 +34,37 @@ export interface UserMenuProps { /** If `true`, disables `data-testid` because it will not be visible. */ readonly hidden?: boolean readonly setPage: (page: pageSwitcher.Page) => void - readonly supportsLocalBackend: boolean readonly onSignOut: () => void } /** Handling the UserMenuItem click event logic and displaying its content. */ export default function UserMenu(props: UserMenuProps) { - const { hidden = false, setPage, supportsLocalBackend, onSignOut } = props + const { hidden = false, setPage, onSignOut } = props const [initialized, setInitialized] = React.useState(false) + const supportsLocalBackend = supportsLocalBackendProvider.useSupportsLocalBackend() const navigate = navigateHooks.useNavigate() const { signOut } = authProvider.useAuth() const { user } = authProvider.useNonPartialUserSession() - const { unsetModal } = modalProvider.useSetModal() + const { setModal, unsetModal } = modalProvider.useSetModal() const { getText } = textProvider.useText() const toastAndLog = toastAndLogHooks.useToastAndLog() - React.useEffect(() => { - requestAnimationFrame(setInitialized.bind(null, true)) + React.useLayoutEffect(() => { + setInitialized(true) }, []) + const aboutThisAppMenuEntry = ( + { + setModal() + }} + /> + ) + return (