Skip to content

Commit 0b21e9b

Browse files
feat: rename active local workspace. (#135)
1 parent b247622 commit 0b21e9b

5 files changed

Lines changed: 218 additions & 4 deletions

File tree

playwright/github-byot-ai.spec.ts

Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {
1818
openStoredWorkspaceContextById,
1919
seedLocalWorkspaceContexts,
2020
} from './github-pr-drawer/github-pr-drawer.helpers.js'
21+
import { selectWorkspacesRepositoryFilter } from './github-pr-drawer/github-pr-drawer.helpers.js'
2122

2223
test('PR/BYOT controls are visible and chat stays hidden until token connect', async ({
2324
page,
@@ -418,6 +419,191 @@ test('Local workspace can be renamed from Workspaces drawer', async ({ page }) =
418419
.toBe(renamedTitle)
419420
})
420421

422+
test('Active Local non-PR workspace can be renamed from Workspaces drawer', async ({
423+
page,
424+
}) => {
425+
const activeWorkspaceId = 'active_local_workspace_rename_allowed'
426+
const originalTitle = 'Active local rename original title'
427+
const renamedTitle = 'Active local rename updated title'
428+
429+
await waitForAppReady(page)
430+
431+
await seedLocalWorkspaceContexts(page, [
432+
{
433+
id: activeWorkspaceId,
434+
repo: '',
435+
workspaceScope: 'local',
436+
head: 'feat/active-local-rename-allowed',
437+
prTitle: originalTitle,
438+
prContextState: 'inactive',
439+
prNumber: null,
440+
tabs: [
441+
{
442+
id: 'component',
443+
path: 'src/component.tsx',
444+
language: 'tsx',
445+
role: 'component',
446+
content: 'export const App = () => <main>active local rename allowed</main>',
447+
order: 0,
448+
source: 'workspace',
449+
dirty: false,
450+
},
451+
],
452+
activeTabId: 'component',
453+
},
454+
])
455+
456+
await page.reload()
457+
await waitForAppReady(page)
458+
459+
const workspacesToggle = page.getByRole('button', {
460+
name: 'Workspaces',
461+
exact: true,
462+
})
463+
await workspacesToggle.click()
464+
465+
const workspaceSelect = page.getByLabel('Stored workspace')
466+
const renameButton = page.getByRole('button', { name: 'Rename', exact: true })
467+
await expect(renameButton).toBeVisible()
468+
469+
await expect(workspaceSelect).toHaveValue(activeWorkspaceId)
470+
await expect(renameButton).toBeEnabled()
471+
472+
page.once('dialog', async dialog => {
473+
expect(dialog.type()).toBe('prompt')
474+
expect(dialog.defaultValue()).toBe(originalTitle)
475+
await dialog.accept(renamedTitle)
476+
})
477+
478+
await renameButton.click()
479+
await expect(page.locator('#workspaces-status')).toContainText('Renamed workspace.')
480+
481+
const records = await getAllWorkspaceRecords(page)
482+
const renamedRecord = records.find(record => record?.id === activeWorkspaceId)
483+
484+
expect(renamedRecord).toBeTruthy()
485+
expect(typeof renamedRecord?.prTitle === 'string' ? renamedRecord.prTitle : '').toBe(
486+
renamedTitle,
487+
)
488+
489+
const selectedLabelText = await page
490+
.locator('#workspaces-select option:checked')
491+
.textContent()
492+
expect(String(selectedLabelText ?? '').trim()).toBe(renamedTitle)
493+
await expect(page.locator('#workspace-context-status')).toContainText(renamedTitle)
494+
})
495+
496+
test('Active Local workspace with active PR context and null PR number cannot be renamed from Workspaces drawer', async ({
497+
page,
498+
}) => {
499+
const activeWorkspaceId = 'active_local_workspace_rename_blocked_pr_associated'
500+
501+
await waitForAppReady(page)
502+
503+
await seedLocalWorkspaceContexts(page, [
504+
{
505+
id: activeWorkspaceId,
506+
repo: '',
507+
workspaceScope: 'local',
508+
head: 'feat/active-local-rename-blocked',
509+
prTitle: 'PR-associated local workspace',
510+
prContextState: 'active',
511+
prNumber: null,
512+
tabs: [
513+
{
514+
id: 'component',
515+
path: 'src/component.tsx',
516+
language: 'tsx',
517+
role: 'component',
518+
content: 'export const App = () => <main>active local rename blocked</main>',
519+
order: 0,
520+
source: 'workspace',
521+
dirty: false,
522+
},
523+
],
524+
activeTabId: 'component',
525+
},
526+
])
527+
528+
await page.reload()
529+
await waitForAppReady(page)
530+
531+
const workspacesToggle = page.getByRole('button', {
532+
name: 'Workspaces',
533+
exact: true,
534+
})
535+
await workspacesToggle.click()
536+
537+
const workspaceSelect = page.getByLabel('Stored workspace')
538+
const renameButton = page.getByRole('button', { name: 'Rename', exact: true })
539+
await expect(renameButton).toBeVisible()
540+
541+
await expect(workspaceSelect).toHaveValue(activeWorkspaceId)
542+
await expect(renameButton).toBeDisabled()
543+
})
544+
545+
test('Repository-scoped workspace cannot be renamed from Workspaces drawer', async ({
546+
page,
547+
}) => {
548+
const repositoryFullName = 'knightedcodemonkey/develop'
549+
const repositoryWorkspaceId = 'repository_workspace_rename_blocked'
550+
551+
await waitForAppReady(page)
552+
553+
await seedLocalWorkspaceContexts(page, [
554+
{
555+
id: repositoryWorkspaceId,
556+
repo: repositoryFullName,
557+
workspaceScope: 'repository',
558+
head: 'feat/repository-rename-blocked',
559+
prTitle: 'Repository scoped workspace',
560+
prContextState: 'inactive',
561+
prNumber: null,
562+
tabs: [
563+
{
564+
id: 'component',
565+
path: 'src/component.tsx',
566+
language: 'tsx',
567+
role: 'component',
568+
content: 'export const App = () => <main>repository rename blocked</main>',
569+
order: 0,
570+
source: 'workspace',
571+
dirty: false,
572+
},
573+
],
574+
activeTabId: 'component',
575+
},
576+
])
577+
578+
await page.route('https://api.github.com/user/repos**', async route => {
579+
await route.fulfill({
580+
status: 200,
581+
contentType: 'application/json',
582+
body: JSON.stringify([
583+
{
584+
id: 11,
585+
owner: { login: 'knightedcodemonkey' },
586+
name: 'develop',
587+
full_name: repositoryFullName,
588+
default_branch: 'main',
589+
permissions: { push: true },
590+
},
591+
]),
592+
})
593+
})
594+
595+
await page.reload()
596+
await waitForAppReady(page)
597+
await connectByotWithSingleRepo(page)
598+
await selectWorkspacesRepositoryFilter(page, repositoryFullName)
599+
600+
const workspaceSelect = page.getByLabel('Stored workspace')
601+
const renameButton = page.getByRole('button', { name: 'Rename', exact: true })
602+
603+
await expect(workspaceSelect).toHaveValue(repositoryWorkspaceId)
604+
await expect(renameButton).toBeHidden()
605+
})
606+
421607
test('chat stays usable after opening a Local workspace with PAT connected', async ({
422608
page,
423609
}) => {

src/app.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -953,6 +953,7 @@ const { syncActiveWorkspaceRepositoryScope, forkWorkspaceFromCurrentState } =
953953
setActiveWorkspacePersistedMetadata: ({ prTitle, head } = {}) => {
954954
activeWorkspacePersistedPrTitle = toNonEmptyWorkspaceText(prTitle)
955955
activeWorkspacePersistedHeadBranch = toNonEmptyWorkspaceText(head)
956+
workspaceContextStatusController.render()
956957
},
957958
})
958959

@@ -1168,6 +1169,11 @@ const githubWorkflows = createGitHubWorkflowsSetup({
11681169
workspaceStorage,
11691170
getActiveWorkspaceRecordId: () => activeWorkspaceRecordId,
11701171
getActiveWorkspaceDisplayLabel,
1172+
setActiveWorkspacePersistedMetadata: ({ prTitle, head } = {}) => {
1173+
activeWorkspacePersistedPrTitle = toNonEmptyWorkspaceText(prTitle)
1174+
activeWorkspacePersistedHeadBranch = toNonEmptyWorkspaceText(head)
1175+
workspaceContextStatusController.render()
1176+
},
11711177
setActiveWorkspaceRecordId,
11721178
setActiveWorkspaceCreatedAt: value => (activeWorkspaceCreatedAt = value),
11731179
buildWorkspaceRecordSnapshot,

src/modules/app-core/github-workflows-setup.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ const createGitHubWorkflowsSetup = ({
2424
setActiveWorkspaceCreatedAt: workspace.setActiveWorkspaceCreatedAt,
2525
buildWorkspaceRecordSnapshot: workspace.buildWorkspaceRecordSnapshot,
2626
getActiveWorkspaceDisplayLabel: workspace.getActiveWorkspaceDisplayLabel,
27+
setActiveWorkspacePersistedMetadata: workspace.setActiveWorkspacePersistedMetadata,
2728
listLocalContextRecords: workspace.listLocalContextRecords,
2829
refreshLocalContextOptions: workspace.refreshLocalContextOptions,
2930
applyWorkspaceRecord: workspace.applyWorkspaceRecord,

src/modules/app-core/github-workflows.js

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ const initializeGitHubWorkflows = ({
5151
workspaceStorage,
5252
getActiveWorkspaceRecordId,
5353
getActiveWorkspaceDisplayLabel,
54+
setActiveWorkspacePersistedMetadata,
5455
setActiveWorkspaceRecordId,
5556
setActiveWorkspaceCreatedAt,
5657
buildWorkspaceRecordSnapshot,
@@ -560,14 +561,22 @@ const initializeGitHubWorkflows = ({
560561
return false
561562
}
562563

563-
if (getActiveWorkspaceRecordId() === workspaceId) {
564+
const workspacePrContextState =
565+
toSafeWorkspaceText(record.prContextState).toLowerCase() || 'inactive'
566+
const hasWorkspacePrNumber =
567+
typeof record.prNumber === 'number' && Number.isFinite(record.prNumber)
568+
const isPrAssociatedWorkspace =
569+
workspacePrContextState !== 'inactive' || hasWorkspacePrNumber
570+
if (isPrAssociatedWorkspace) {
564571
workspacesDrawerController?.setStatus(
565-
'Open a different workspace before renaming this one.',
572+
'Use Pull Request controls to rename PR-associated workspaces.',
566573
'error',
567574
)
568575
return false
569576
}
570577

578+
const isActiveWorkspace = getActiveWorkspaceRecordId() === workspaceId
579+
571580
const currentWorkspaceName =
572581
toSafeWorkspaceText(record.prTitle) ||
573582
toSafeWorkspaceText(record.head) ||
@@ -601,6 +610,16 @@ const initializeGitHubWorkflows = ({
601610
lastModified: Date.now(),
602611
})
603612

613+
if (
614+
isActiveWorkspace &&
615+
typeof setActiveWorkspacePersistedMetadata === 'function'
616+
) {
617+
setActiveWorkspacePersistedMetadata({
618+
prTitle: nextWorkspaceName,
619+
head: toSafeWorkspaceText(record.head),
620+
})
621+
}
622+
604623
await refreshLocalContextOptions()
605624
return true
606625
} catch {

src/modules/workspace/workspaces-drawer/drawer.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ export const createWorkspacesDrawer = ({
7878
}
7979

8080
const isInactiveWithoutPrNumber = workspace => {
81-
const state = toSafeText(workspace?.prContextState).toLowerCase()
81+
const state = toSafeText(workspace?.prContextState).toLowerCase() || 'inactive'
8282
const hasPrNumber =
8383
typeof workspace?.prNumber === 'number' && Number.isFinite(workspace.prNumber)
8484
return state === 'inactive' && !hasPrNumber
@@ -157,14 +157,16 @@ export const createWorkspacesDrawer = ({
157157
const selectedWorkspaceScope = toSafeWorkspaceScope(selectedEntry)
158158
const isSelectedLocalWorkspace =
159159
hasSelection && selectedWorkspaceScope === localWorkspaceScopeValue
160+
const isSelectedWorkspaceNonPr =
161+
hasSelection && isInactiveWithoutPrNumber(selectedEntry)
160162
const isSelectedWorkspaceActive =
161163
hasSelection &&
162164
Boolean(activeWorkspaceId) &&
163165
normalizedSelectedId === activeWorkspaceId
164166
const canRenameWorkspace =
165167
typeof onRenameSelected === 'function' &&
166168
isSelectedLocalWorkspace &&
167-
!isSelectedWorkspaceActive
169+
isSelectedWorkspaceNonPr
168170
const canCreateWorkspace = typeof onCreateWorkspace === 'function'
169171
const canInitializeWorkspace = typeof onInitializeWorkspace === 'function'
170172
const hasStoredWorkspaces =

0 commit comments

Comments
 (0)