Skip to content

Commit dd6ef68

Browse files
refactor: pr dependent dropdowns, toast position on mobile.
1 parent d99740b commit dd6ef68

6 files changed

Lines changed: 350 additions & 13 deletions

File tree

playwright/app.spec.ts

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,17 @@ const connectByotWithSingleRepo = async (page: Page) => {
165165
})
166166
})
167167

168+
await page.route(
169+
'https://api.github.com/repos/knightedcodemonkey/develop/branches**',
170+
async route => {
171+
await route.fulfill({
172+
status: 200,
173+
contentType: 'application/json',
174+
body: JSON.stringify([{ name: 'main' }, { name: 'release' }]),
175+
})
176+
},
177+
)
178+
168179
await page.locator('#github-token-input').fill('github_pat_fake_chat_1234567890')
169180
await page.locator('#github-token-add').click()
170181
await expect(page.locator('#status')).toHaveText('Loaded 1 writable repositories')
@@ -717,6 +728,82 @@ test('Open PR drawer confirms and submits component/styles filepaths', async ({
717728
)
718729
})
719730

731+
test('Open PR drawer base dropdown updates from mocked repo branches', async ({
732+
page,
733+
}) => {
734+
const branchRequestUrls: string[] = []
735+
736+
await page.route('https://api.github.com/user/repos**', async route => {
737+
await route.fulfill({
738+
status: 200,
739+
contentType: 'application/json',
740+
body: JSON.stringify([
741+
{
742+
id: 2,
743+
owner: { login: 'knightedcodemonkey' },
744+
name: 'develop',
745+
full_name: 'knightedcodemonkey/develop',
746+
default_branch: 'main',
747+
permissions: { push: true },
748+
},
749+
{
750+
id: 1,
751+
owner: { login: 'knightedcodemonkey' },
752+
name: 'css',
753+
full_name: 'knightedcodemonkey/css',
754+
default_branch: 'stable',
755+
permissions: { push: true },
756+
},
757+
]),
758+
})
759+
})
760+
761+
await page.route('https://api.github.com/repos/**/branches**', async route => {
762+
const url = route.request().url()
763+
branchRequestUrls.push(url)
764+
765+
const branchNames = url.includes('/repos/knightedcodemonkey/css/branches')
766+
? ['stable', 'release/1.x']
767+
: ['main', 'develop-next']
768+
769+
await route.fulfill({
770+
status: 200,
771+
contentType: 'application/json',
772+
body: JSON.stringify(branchNames.map(name => ({ name }))),
773+
})
774+
})
775+
776+
await waitForAppReady(page, `${appEntryPath}?feature-ai=true`)
777+
778+
await page.locator('#github-token-input').fill('github_pat_fake_1234567890')
779+
await page.locator('#github-token-add').click()
780+
await expect(page.locator('#status')).toHaveText('Loaded 2 writable repositories')
781+
782+
await ensureOpenPrDrawerOpen(page)
783+
784+
const repoSelect = page.locator('#github-pr-repo-select')
785+
const baseSelect = page.locator('#github-pr-base-branch')
786+
787+
await repoSelect.selectOption('knightedcodemonkey/develop')
788+
await expect(baseSelect).toHaveValue('main')
789+
await expect(baseSelect.locator('option')).toHaveText(['main', 'develop-next'])
790+
791+
await repoSelect.selectOption('knightedcodemonkey/css')
792+
await expect(baseSelect).toHaveValue('stable')
793+
await expect(baseSelect.locator('option')).toHaveText(['stable', 'release/1.x'])
794+
795+
expect(
796+
branchRequestUrls.some(url =>
797+
url.includes('https://api.github.com/repos/knightedcodemonkey/develop/branches'),
798+
),
799+
).toBe(true)
800+
expect(
801+
branchRequestUrls.some(url =>
802+
url.includes('https://api.github.com/repos/knightedcodemonkey/css/branches'),
803+
),
804+
).toBe(true)
805+
})
806+
720807
test('Open PR drawer validates unsafe filepaths', async ({ page }) => {
721808
await waitForAppReady(page, `${appEntryPath}?feature-ai=true`)
722809
await connectByotWithSingleRepo(page)

src/index.html

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -659,13 +659,13 @@ <h2>Open Pull Request</h2>
659659
for="github-pr-base-branch"
660660
>
661661
<span>Base</span>
662-
<input
662+
<select
663663
id="github-pr-base-branch"
664-
type="text"
665-
autocomplete="off"
666-
spellcheck="false"
667-
placeholder="main"
668-
/>
664+
aria-label="Pull request base branch"
665+
disabled
666+
>
667+
<option value="main" selected>main</option>
668+
</select>
669669
</label>
670670

671671
<label

src/modules/github-api.js

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,14 @@ const normalizeRepo = repo => {
107107

108108
const hasWritePermission = permissions => Boolean(permissions && permissions.push)
109109

110+
const normalizeBranchName = branch => {
111+
if (!branch || typeof branch !== 'object') {
112+
return null
113+
}
114+
115+
return typeof branch.name === 'string' && branch.name.trim() ? branch.name : null
116+
}
117+
110118
const buildRequestHeaders = token => ({
111119
Accept: 'application/vnd.github+json',
112120
Authorization: `Bearer ${token}`,
@@ -363,6 +371,19 @@ const listReposPage = async ({ token, url, signal }) => {
363371
}
364372
}
365373

374+
const listBranchesPage = async ({ token, url, signal }) => {
375+
const { data, nextPageUrl } = await fetchJson({ token, url, signal })
376+
377+
if (!Array.isArray(data)) {
378+
throw new Error('Unexpected response while loading repository branches from GitHub.')
379+
}
380+
381+
return {
382+
branches: data.map(normalizeBranchName).filter(Boolean),
383+
nextPageUrl,
384+
}
385+
}
386+
366387
export const listWritableRepositories = async ({ token, signal }) => {
367388
if (typeof token !== 'string' || token.trim().length === 0) {
368389
throw new Error('A GitHub token is required to load repositories.')
@@ -394,6 +415,41 @@ export const listWritableRepositories = async ({ token, signal }) => {
394415
return writableRepos
395416
}
396417

418+
export const listRepositoryBranches = async ({ token, owner, repo, signal }) => {
419+
if (typeof token !== 'string' || token.trim().length === 0) {
420+
throw new Error('A GitHub token is required to load branches.')
421+
}
422+
423+
if (typeof owner !== 'string' || !owner || typeof repo !== 'string' || !repo) {
424+
throw new Error('A valid repository owner/name is required to load branches.')
425+
}
426+
427+
const branches = []
428+
const dedupe = new Set()
429+
let nextPageUrl = `${githubApiBaseUrl}/repos/${owner}/${repo}/branches?per_page=100`
430+
let remainingPageBudget = 5
431+
432+
while (nextPageUrl && remainingPageBudget > 0) {
433+
/* Branch pagination depends on the prior Link header response. */
434+
// eslint-disable-next-line no-await-in-loop
435+
const page = await listBranchesPage({ token, url: nextPageUrl, signal })
436+
for (const name of page.branches) {
437+
if (dedupe.has(name)) {
438+
continue
439+
}
440+
441+
dedupe.add(name)
442+
branches.push(name)
443+
}
444+
445+
nextPageUrl = page.nextPageUrl
446+
remainingPageBudget -= 1
447+
}
448+
449+
branches.sort((left, right) => left.localeCompare(right))
450+
return branches
451+
}
452+
397453
export const streamGitHubChatCompletion = async ({
398454
token,
399455
messages,

0 commit comments

Comments
 (0)