diff --git a/src/renderer/src/components/new-workspace/NewWorkspaceComposerProjectSection.add-project.test.tsx b/src/renderer/src/components/new-workspace/NewWorkspaceComposerProjectSection.add-project.test.tsx index 02639b9a238..3adbcbc692d 100644 --- a/src/renderer/src/components/new-workspace/NewWorkspaceComposerProjectSection.add-project.test.tsx +++ b/src/renderer/src/components/new-workspace/NewWorkspaceComposerProjectSection.add-project.test.tsx @@ -25,6 +25,7 @@ function cloudOnly( picked: null, folderProject: false, needsGitHubPick: false, + remoteServerProject: false, githubStep: null, pick: () => {}, clear: () => {}, diff --git a/src/renderer/src/components/new-workspace/NewWorkspaceComposerProjectSection.fixture.tsx b/src/renderer/src/components/new-workspace/NewWorkspaceComposerProjectSection.fixture.tsx index 89cd74fef6c..e54d3b3b8be 100644 --- a/src/renderer/src/components/new-workspace/NewWorkspaceComposerProjectSection.fixture.tsx +++ b/src/renderer/src/components/new-workspace/NewWorkspaceComposerProjectSection.fixture.tsx @@ -99,6 +99,7 @@ const handoffCloudStart = (githubHandoff: 'waiting' | 'timed-out'): CloudStartVi picked: null, folderProject: false, needsGitHubPick: false, + remoteServerProject: false, githubStep: null, pick: () => {}, clear: () => {}, diff --git a/src/renderer/src/components/new-workspace/NewWorkspaceComposerProjectSection.tsx b/src/renderer/src/components/new-workspace/NewWorkspaceComposerProjectSection.tsx index 948b0a9839d..551bfef169a 100644 --- a/src/renderer/src/components/new-workspace/NewWorkspaceComposerProjectSection.tsx +++ b/src/renderer/src/components/new-workspace/NewWorkspaceComposerProjectSection.tsx @@ -236,6 +236,8 @@ export function NewWorkspaceComposerProjectSection({ } triggerClassName="h-9 w-full border-input text-sm focus-visible:border-focus-ring focus-visible:ring-1 focus-visible:ring-focus-ring" invalid={Boolean(projectError) || Boolean(needsRepo)} + // Waiting for a GitHub repository is a fixable precondition: amber, not red. + invalidTone={!projectError && needsRepo ? 'warning' : 'error'} describedBy={ !projectError && needsRepo ? ORCA_CLOUD_NEEDS_REPO_ID : projectDescriptionId } @@ -255,6 +257,8 @@ export function NewWorkspaceComposerProjectSection({ ) : projectOptions.length === 0 ? (

diff --git a/src/renderer/src/components/new-workspace/ProjectCombobox.tsx b/src/renderer/src/components/new-workspace/ProjectCombobox.tsx index 5927bfae436..52e4b34c265 100644 --- a/src/renderer/src/components/new-workspace/ProjectCombobox.tsx +++ b/src/renderer/src/components/new-workspace/ProjectCombobox.tsx @@ -23,7 +23,7 @@ import { import { ProjectOptionMark, ProjectOptionRow } from './ProjectComboboxRow' import { useRecentProjectIds } from './use-recent-project-ids' import { isWithinComboboxRoot, useTypeAheadCombobox } from './use-type-ahead-combobox' -import type { ProjectComboboxProps } from './project-combobox-props' +import { projectFieldInvalidClass, type ProjectComboboxProps } from './project-combobox-props' import { COMBOBOX_FIELD_SHELL, COMBOBOX_POPOVER_SURFACE } from './type-ahead-combobox-styles' import { cloudRepoKey, @@ -48,6 +48,7 @@ export default function ProjectCombobox({ placeholder = 'Choose project', triggerClassName, invalid = false, + invalidTone = 'error', describedBy, cloudRepos = null, cloudRepoValue = null, @@ -218,9 +219,8 @@ export default function ProjectCombobox({ }} className={cn( COMBOBOX_FIELD_SHELL, - // Invalid stays red while focused, so the field and its error read as one. - invalid && - 'border-destructive ring-destructive/20 focus-within:border-destructive focus-within:ring-destructive/40 dark:ring-destructive/40', + // Invalid stays marked while focused, so the field and its line read as one. + invalid && projectFieldInvalidClass(invalidTone), triggerClassName )} > diff --git a/src/renderer/src/components/new-workspace/project-combobox-props.ts b/src/renderer/src/components/new-workspace/project-combobox-props.ts index 72401f1d6fe..0b37d12577e 100644 --- a/src/renderer/src/components/new-workspace/project-combobox-props.ts +++ b/src/renderer/src/components/new-workspace/project-combobox-props.ts @@ -13,6 +13,11 @@ export type ProjectComboboxProps = { placeholder?: string triggerClassName?: string invalid?: boolean + /** + * `warning`: a fixable precondition (Orca Cloud waiting for a GitHub repository), ringed amber + * rather than red; still aria-invalid for screen readers. Defaults to `error`. + */ + invalidTone?: 'error' | 'warning' describedBy?: string /** D4: GitHub repositories with no clone here, listed while Orca Cloud is the Run on target. */ cloudRepos?: readonly OrcaCloudGitHubRepository[] | null @@ -32,3 +37,10 @@ export type ProjectComboboxProps = { */ focusSignal?: number } + +/** The field's invalid ring: red for an error, amber for a fixable precondition (glossary). */ +export function projectFieldInvalidClass(tone: 'error' | 'warning'): string { + return tone === 'warning' + ? 'border-status-warning ring-status-warning/20 focus-within:border-status-warning focus-within:ring-status-warning/40 dark:ring-status-warning/40' + : 'border-destructive ring-destructive/20 focus-within:border-destructive focus-within:ring-destructive/40 dark:ring-destructive/40' +} diff --git a/src/renderer/src/components/new-workspace/run-on/OrcaCloudNeedsRepoLine.tsx b/src/renderer/src/components/new-workspace/run-on/OrcaCloudNeedsRepoLine.tsx index ccf006cba43..bf3859a60b3 100644 --- a/src/renderer/src/components/new-workspace/run-on/OrcaCloudNeedsRepoLine.tsx +++ b/src/renderer/src/components/new-workspace/run-on/OrcaCloudNeedsRepoLine.tsx @@ -10,12 +10,44 @@ import { ORCA_CLOUD_NEEDS_REPO_ID } from './cloud-start-view' */ export function OrcaCloudNeedsRepoLine({ hasRepos, - onAddRepository + onAddRepository, + unreachable = false, + onMoreAccess }: { hasRepos: boolean onAddRepository: () => void + /** UNREACHABLE-REPO-NO-ROUTE: the project's own GitHub repository is out of Orca Cloud's reach. */ + unreachable?: boolean + /** Opens GitHub to give Orca access to more repositories ("Give Orca access"). */ + onMoreAccess?: () => void }): React.JSX.Element { const t = useTranslate() + if (unreachable) { + return ( +

+ ) + } return (
-

+

{t( 'components.newWorkspace.cloudOnly.needsRepository', 'Orca Cloud needs a GitHub repository.' diff --git a/src/renderer/src/components/new-workspace/run-on/OrcaCloudNeedsRepoSubmit.fixture.tsx b/src/renderer/src/components/new-workspace/run-on/OrcaCloudNeedsRepoSubmit.fixture.tsx index 7a6370ac1cb..8a947d9b74d 100644 --- a/src/renderer/src/components/new-workspace/run-on/OrcaCloudNeedsRepoSubmit.fixture.tsx +++ b/src/renderer/src/components/new-workspace/run-on/OrcaCloudNeedsRepoSubmit.fixture.tsx @@ -37,17 +37,40 @@ const orcaCloud: EphemeralVmRecipeOption = { create: 'orca-cloud create' } const NEEDS_REPO = { hasRepos: true } +// UNREACHABLE-REPO-NO-ROUTE: Hello-World cloned only on a paired server, out of Orca Cloud's reach. +const UNREACHABLE = { hasRepos: true, unreachable: true } -function Composer(): React.JSX.Element { +function Composer({ unreachable = false }: { unreachable?: boolean }): React.JSX.Element { const [project, setProject] = useState('folder') const [created, setCreated] = useState(false) - const needsRepo = project === 'folder' ? NEEDS_REPO : null + const needsRepo = project === 'folder' ? (unreachable ? UNREACHABLE : NEEDS_REPO) : null const guard = useNeedsRepoSubmitGuard(needsRepo, () => setCreated(true)) const cloudStart = { onSelectOrcaCloud: () => {}, onUserPick: () => {}, folderProject: project === 'folder', - needsGitHubRepo: guard.shown + needsGitHubRepo: guard.shown, + ...(unreachable + ? { + cloudOnly: { + repos: [], + showMore: null, + retryRepos: null, + retryingRepos: false, + picked: null, + folderProject: false, + needsGitHubPick: true, + remoteServerProject: true, + githubStep: null, + pick: () => {}, + clear: () => {}, + moreAccess: () => {}, + githubHandoff: 'idle' as const, + checkGitHubAgain: () => {}, + start: async () => false + } + } + : {}) } return (

@@ -99,7 +122,8 @@ function Composer(): React.JSX.Element { const wait = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)) const clickCreate = (frame: HTMLElement): void => [...frame.querySelectorAll('button')] - .find((button) => button.textContent?.includes('Create workspace')) + // The footer's primary reads "Create" (its shortcut chip follows). + .find((button) => /^Create\b/.test(button.textContent?.trim() ?? '')) ?.click() const base = { @@ -131,3 +155,12 @@ export const fixed = defineFixture({ await wait(200) } }) +/** A remote-server project whose GitHub repository Orca Cloud can't reach, after Create. */ +export const unreachableAfterSubmit = defineFixture({ + ...base, + render: () => , + interact: async (frame) => { + clickCreate(frame) + await wait(200) + } +}) diff --git a/src/renderer/src/components/new-workspace/run-on/cloud-start-view.ts b/src/renderer/src/components/new-workspace/run-on/cloud-start-view.ts index f61a1ca8391..00135de3a03 100644 --- a/src/renderer/src/components/new-workspace/run-on/cloud-start-view.ts +++ b/src/renderer/src/components/new-workspace/run-on/cloud-start-view.ts @@ -16,7 +16,7 @@ export type CloudStartView = { * Project. `hasRepos`: any to choose from. `attempt` counts those stopped Creates (focus moves to * Project on each); absent until the first. */ - needsGitHubRepo?: { hasRepos: boolean; attempt?: number } | null + needsGitHubRepo?: { hasRepos: boolean; unreachable?: boolean; attempt?: number } | null /** Bumped to open the Project picker at its GitHub repositories (CLONE-ACCESS-ERROR's Choose another). */ openRepoPickerSignal?: number } @@ -32,10 +32,19 @@ export function orcaCloudNeedsGitHubRepo(args: { pickedRepo: boolean repoCount: number githubProjectCount: number -}): { hasRepos: boolean } | null { - return args.cloudSelected && args.folderProject && !args.pickedRepo - ? { hasRepos: args.repoCount > 0 || args.githubProjectCount > 0 } - : null + /** + * UNREACHABLE-REPO-NO-ROUTE: a GitHub project cloned only on a remote server whose repository + * Orca Cloud can't reach. The line says so and offers More access, instead of the folder wording. + */ + remoteServerProject?: boolean +}): { hasRepos: boolean; unreachable?: true } | null { + if (!args.cloudSelected || !args.folderProject || args.pickedRepo) { + return null + } + return { + hasRepos: args.repoCount > 0 || args.githubProjectCount > 0, + ...(args.remoteServerProject ? { unreachable: true as const } : {}) + } } /** A project that can start in Orca Cloud: git, with a GitHub remote (or one not read yet). */ diff --git a/src/renderer/src/components/new-workspace/run-on/orca-cloud-unreachable-repo.test.tsx b/src/renderer/src/components/new-workspace/run-on/orca-cloud-unreachable-repo.test.tsx new file mode 100644 index 00000000000..cf86f479f06 --- /dev/null +++ b/src/renderer/src/components/new-workspace/run-on/orca-cloud-unreachable-repo.test.tsx @@ -0,0 +1,55 @@ +// @vitest-environment happy-dom +// UNREACHABLE-REPO-NO-ROUTE: Orca Cloud on a GitHub project cloned only on a remote server, whose +// repository the team's GitHub access doesn't cover. Create waits, and the line under Project says +// why (not the folder wording) with More access; the Project picker lists repositories to pick. +import { cleanup, fireEvent, render, screen } from '@testing-library/react' +import { afterEach, describe, expect, it, vi } from 'vitest' +import { orcaCloudNeedsGitHubRepo } from './cloud-start-view' +import { OrcaCloudNeedsRepoLine } from './OrcaCloudNeedsRepoLine' + +afterEach(cleanup) + +const base = { + cloudSelected: true, + folderProject: true, + pickedRepo: false, + repoCount: 2, + githubProjectCount: 1 +} + +describe('unreachable repository on a remote-server project', () => { + it('marks the wait as unreachable only for a remote-server project', () => { + expect(orcaCloudNeedsGitHubRepo({ ...base, remoteServerProject: true })).toEqual({ + hasRepos: true, + unreachable: true + }) + expect(orcaCloudNeedsGitHubRepo(base)).toEqual({ hasRepos: true }) + // Reachable (auto-picked): nothing waits. + expect( + orcaCloudNeedsGitHubRepo({ ...base, remoteServerProject: true, pickedRepo: true }) + ).toBeNull() + }) + + it('says Orca Cloud cannot reach the repository and offers Give Orca access, in a muted tone', () => { + const onMoreAccess = vi.fn() + render( + {}} + unreachable + onMoreAccess={onMoreAccess} + /> + ) + expect(screen.getByRole('alert').textContent).toContain('can’t reach this project’s') + expect(screen.queryByText('Orca Cloud needs a GitHub repository.')).toBeNull() + // A fixable access precondition, not an error: never red. + expect(screen.getByRole('alert').querySelector('p')?.className).not.toContain('destructive') + fireEvent.click(screen.getByRole('button', { name: 'Give Orca access' })) + expect(onMoreAccess).toHaveBeenCalledTimes(1) + }) + + it('a folder keeps its own wording', () => { + render( {}} />) + expect(screen.getByRole('alert').textContent).toBe('Orca Cloud needs a GitHub repository.') + }) +}) diff --git a/src/renderer/src/components/new-workspace/run-on/use-cloud-only-repo.ts b/src/renderer/src/components/new-workspace/run-on/use-cloud-only-repo.ts index 7eac4f9db25..2ec95248dc5 100644 --- a/src/renderer/src/components/new-workspace/run-on/use-cloud-only-repo.ts +++ b/src/renderer/src/components/new-workspace/run-on/use-cloud-only-repo.ts @@ -34,6 +34,8 @@ export type CloudOnlyRepo = { * cloned only on a remote server (RUNON-NO-NEW-SANDBOX). Create waits until one is picked. */ needsGitHubPick: boolean + /** A GitHub project cloned only on a remote server (RUNON-NO-NEW-SANDBOX). */ + remoteServerProject: boolean /** The GitHub row is done for a repository listed from the team's installations. */ githubStep: CloudSetupStep | null pick: (fullName: string) => void @@ -140,6 +142,7 @@ export function useCloudOnlyRepo({ githubStep: githubStep ?? folderStep ?? (remoteServer ? remoteServerStep() : null), folderProject, needsGitHubPick: folderProject || remoteServer, + remoteServerProject: remoteServer, pick, clear, moreAccess, @@ -159,8 +162,8 @@ function remoteServerStep(): CloudSetupStep { label: translate('orcaCloud.setup.github.repositoryLabel', 'GitHub repository'), status: translate('orcaCloud.setup.github.neededStatus', 'Needed'), detail: translate( - 'components.newWorkspace.cloudOnly.remoteServerDetail', - 'Orca Cloud starts from a GitHub repository and can’t reach this project’s. Give Orca access with More access, or pick another repository.' + 'components.newWorkspace.cloudOnly.remoteServerUnreachable', + 'Orca Cloud starts from a GitHub repository and can’t reach this project’s. Give Orca access to it, or pick another repository.' ) } } diff --git a/src/renderer/src/components/new-workspace/run-on/use-cloud-start-composer.ts b/src/renderer/src/components/new-workspace/run-on/use-cloud-start-composer.ts index fb5f4953159..88c77fd57be 100644 --- a/src/renderer/src/components/new-workspace/run-on/use-cloud-start-composer.ts +++ b/src/renderer/src/components/new-workspace/run-on/use-cloud-start-composer.ts @@ -222,6 +222,7 @@ export function useCloudStartComposer({ needsGitHubRepo: orcaCloudNeedsGitHubRepo({ cloudSelected, folderProject: cloudOnly.needsGitHubPick, + remoteServerProject: cloudOnly.remoteServerProject, pickedRepo: cloudOnly.picked !== null, repoCount: cloudOnly.repos?.length ?? 0, githubProjectCount diff --git a/src/renderer/src/i18n/en-runtime-required.json b/src/renderer/src/i18n/en-runtime-required.json index e3243ae9c76..605ac027522 100644 --- a/src/renderer/src/i18n/en-runtime-required.json +++ b/src/renderer/src/i18n/en-runtime-required.json @@ -2731,13 +2731,6 @@ "sendingNotConfigured": "sending diagnostics is not configured for this build" } }, - "enterprise": { - "audit": { - "action": { - "wsOwnerNotified": "Notified the previous owner about an owner change" - } - } - }, "groups": { "orcaCloud": "Orca Cloud" }, diff --git a/src/renderer/src/i18n/locales/en.json b/src/renderer/src/i18n/locales/en.json index d954b701df3..ccfbd3ce3bd 100644 --- a/src/renderer/src/i18n/locales/en.json +++ b/src/renderer/src/i18n/locales/en.json @@ -24287,7 +24287,7 @@ "theAgent": "The agent", "showMore": "Show more repositories…", "folderDetail": "Orca Cloud starts from a GitHub repository. This project is a folder on this computer.", - "remoteServerDetail": "Orca Cloud starts from a GitHub repository and can’t reach this project’s. Give Orca access with More access, or pick another repository.", + "remoteServerUnreachable": "Orca Cloud starts from a GitHub repository and can’t reach this project’s. Give Orca access to it, or pick another repository.", "headingShort": "GitHub", "badge": "Cloud only. Orca Cloud clones it, so no copy on this computer is needed.", "rowPrivate": "{{repo}}, private, cloud only",