Skip to content

Commit 9cd4399

Browse files
refactor: only one pr context in storage. (#32)
1 parent 99284c1 commit 9cd4399

5 files changed

Lines changed: 191 additions & 12 deletions

File tree

playwright/app.spec.ts

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -830,6 +830,154 @@ test('Open PR drawer base dropdown updates from mocked repo branches', async ({
830830
).toBe(true)
831831
})
832832

833+
test('Open PR drawer keeps a single active PR context in localStorage', async ({
834+
page,
835+
}) => {
836+
await page.route('https://api.github.com/user/repos**', async route => {
837+
await route.fulfill({
838+
status: 200,
839+
contentType: 'application/json',
840+
body: JSON.stringify([
841+
{
842+
id: 2,
843+
owner: { login: 'knightedcodemonkey' },
844+
name: 'develop',
845+
full_name: 'knightedcodemonkey/develop',
846+
default_branch: 'main',
847+
permissions: { push: true },
848+
},
849+
{
850+
id: 1,
851+
owner: { login: 'knightedcodemonkey' },
852+
name: 'css',
853+
full_name: 'knightedcodemonkey/css',
854+
default_branch: 'stable',
855+
permissions: { push: true },
856+
},
857+
]),
858+
})
859+
})
860+
861+
await mockRepositoryBranches(page, {
862+
'knightedcodemonkey/develop': ['main', 'develop-next'],
863+
'knightedcodemonkey/css': ['stable', 'release/1.x'],
864+
})
865+
866+
await waitForAppReady(page, `${appEntryPath}?feature-ai=true`)
867+
868+
await page.locator('#github-token-input').fill('github_pat_fake_1234567890')
869+
await page.locator('#github-token-add').click()
870+
await ensureOpenPrDrawerOpen(page)
871+
872+
const repoSelect = page.locator('#github-pr-repo-select')
873+
const componentPath = page.locator('#github-pr-component-path')
874+
875+
await repoSelect.selectOption('knightedcodemonkey/develop')
876+
await componentPath.fill('examples/develop/App.tsx')
877+
await componentPath.blur()
878+
879+
await repoSelect.selectOption('knightedcodemonkey/css')
880+
await componentPath.fill('examples/css/App.tsx')
881+
await componentPath.blur()
882+
883+
const activeContext = await page.evaluate(() => {
884+
const storagePrefix = 'knighted:develop:github-pr-config:'
885+
const keys = Object.keys(localStorage).filter(key => key.startsWith(storagePrefix))
886+
const key = keys[0] ?? null
887+
const raw = key ? localStorage.getItem(key) : null
888+
889+
let parsed = null
890+
try {
891+
parsed = raw ? JSON.parse(raw) : null
892+
} catch {
893+
parsed = null
894+
}
895+
896+
return { keys, key, parsed }
897+
})
898+
899+
expect(activeContext.keys).toHaveLength(1)
900+
expect(activeContext.key).toBe(
901+
'knighted:develop:github-pr-config:knightedcodemonkey/css',
902+
)
903+
expect(activeContext.parsed?.componentFilePath).toBe('examples/css/App.tsx')
904+
})
905+
906+
test('Open PR drawer does not prune saved PR context on repo switch before save', async ({
907+
page,
908+
}) => {
909+
await page.route('https://api.github.com/user/repos**', async route => {
910+
await route.fulfill({
911+
status: 200,
912+
contentType: 'application/json',
913+
body: JSON.stringify([
914+
{
915+
id: 2,
916+
owner: { login: 'knightedcodemonkey' },
917+
name: 'develop',
918+
full_name: 'knightedcodemonkey/develop',
919+
default_branch: 'main',
920+
permissions: { push: true },
921+
},
922+
{
923+
id: 1,
924+
owner: { login: 'knightedcodemonkey' },
925+
name: 'css',
926+
full_name: 'knightedcodemonkey/css',
927+
default_branch: 'stable',
928+
permissions: { push: true },
929+
},
930+
]),
931+
})
932+
})
933+
934+
await mockRepositoryBranches(page, {
935+
'knightedcodemonkey/develop': ['main', 'develop-next'],
936+
'knightedcodemonkey/css': ['stable', 'release/1.x'],
937+
})
938+
939+
await waitForAppReady(page, `${appEntryPath}?feature-ai=true`)
940+
941+
await page.locator('#github-token-input').fill('github_pat_fake_1234567890')
942+
await page.locator('#github-token-add').click()
943+
await ensureOpenPrDrawerOpen(page)
944+
945+
const repoSelect = page.locator('#github-pr-repo-select')
946+
const componentPath = page.locator('#github-pr-component-path')
947+
948+
await repoSelect.selectOption('knightedcodemonkey/develop')
949+
await componentPath.fill('examples/develop/App.tsx')
950+
await componentPath.blur()
951+
952+
await repoSelect.selectOption('knightedcodemonkey/css')
953+
954+
const contexts = await page.evaluate(() => {
955+
const storagePrefix = 'knighted:develop:github-pr-config:'
956+
const keys = Object.keys(localStorage)
957+
.filter(key => key.startsWith(storagePrefix))
958+
.sort((left, right) => left.localeCompare(right))
959+
960+
return keys.map(key => {
961+
const raw = localStorage.getItem(key)
962+
let parsed = null
963+
964+
try {
965+
parsed = raw ? JSON.parse(raw) : null
966+
} catch {
967+
parsed = null
968+
}
969+
970+
return { key, parsed }
971+
})
972+
})
973+
974+
expect(contexts).toHaveLength(1)
975+
expect(contexts[0]?.key).toBe(
976+
'knighted:develop:github-pr-config:knightedcodemonkey/develop',
977+
)
978+
expect(contexts[0]?.parsed?.componentFilePath).toBe('examples/develop/App.tsx')
979+
})
980+
833981
test('Open PR drawer validates unsafe filepaths', async ({ page }) => {
834982
await waitForAppReady(page, `${appEntryPath}?feature-ai=true`)
835983
await connectByotWithSingleRepo(page)

src/modules/github-pr-drawer.js

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,35 @@ const defaultPrConfig = {
77
stylesFilePath: 'src/styles/app.css',
88
}
99

10+
const pruneRepositoryPrConfigs = repositoryFullName => {
11+
if (typeof repositoryFullName !== 'string' || !repositoryFullName.trim()) {
12+
return
13+
}
14+
15+
const activeStorageKey = `${prConfigStoragePrefix}${repositoryFullName}`
16+
17+
try {
18+
const keysToRemove = []
19+
20+
for (let index = 0; index < localStorage.length; index += 1) {
21+
const key = localStorage.key(index)
22+
if (!key || !key.startsWith(prConfigStoragePrefix)) {
23+
continue
24+
}
25+
26+
if (key !== activeStorageKey) {
27+
keysToRemove.push(key)
28+
}
29+
}
30+
31+
for (const key of keysToRemove) {
32+
localStorage.removeItem(key)
33+
}
34+
} catch {
35+
/* noop */
36+
}
37+
}
38+
1039
const readRepositoryPrConfig = repositoryFullName => {
1140
if (typeof repositoryFullName !== 'string' || !repositoryFullName.trim()) {
1241
return {}
@@ -31,10 +60,11 @@ const saveRepositoryPrConfig = ({ repositoryFullName, config }) => {
3160
}
3261

3362
try {
34-
localStorage.setItem(
35-
`${prConfigStoragePrefix}${repositoryFullName}`,
36-
JSON.stringify(config),
37-
)
63+
const activeStorageKey = `${prConfigStoragePrefix}${repositoryFullName}`
64+
65+
localStorage.setItem(activeStorageKey, JSON.stringify(config))
66+
67+
pruneRepositoryPrConfigs(repositoryFullName)
3868
} catch {
3969
/* noop */
4070
}

src/modules/type-diagnostics.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -483,7 +483,7 @@ export const createTypeDiagnosticsController = ({
483483
return reactTypeLoadPromise
484484
}
485485

486-
reactTypeLoadPromise = (async () => {
486+
const loadReactTypeFiles = async () => {
487487
const files = new Map()
488488
const packageEntryByName = new Map()
489489
const packageManifestByName = new Map()
@@ -631,7 +631,9 @@ export const createTypeDiagnosticsController = ({
631631
files,
632632
packageEntries: packageEntryByName,
633633
}
634-
})()
634+
}
635+
636+
reactTypeLoadPromise = loadReactTypeFiles()
635637

636638
try {
637639
return await reactTypeLoadPromise

src/styles/ai-controls.css

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -100,8 +100,8 @@
100100
opacity: 0.9;
101101
background: transparent;
102102
padding: 0;
103-
width: 24px;
104-
height: 24px;
103+
width: 20px;
104+
height: 20px;
105105
font-size: 0.74rem;
106106
transition:
107107
border-color 140ms ease,

src/styles/dialogs-overlays.css

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,8 @@
102102

103103
.app-toast {
104104
position: fixed;
105-
top: 76px;
105+
top: auto;
106+
bottom: 12px;
106107
right: 24px;
107108
max-width: min(560px, calc(100vw - 48px));
108109
border-radius: 12px;
@@ -115,7 +116,7 @@
115116
line-height: 1.35;
116117
z-index: 140;
117118
opacity: 0;
118-
transform: translateY(-6px);
119+
transform: translateY(6px);
119120
transition:
120121
opacity 170ms ease,
121122
transform 170ms ease;
@@ -129,8 +130,6 @@
129130

130131
@media (max-width: 900px) {
131132
.app-toast {
132-
top: auto;
133-
bottom: 12px;
134133
left: 12px;
135134
right: 12px;
136135
max-width: none;

0 commit comments

Comments
 (0)