Skip to content

Commit e0d8b67

Browse files
feat: open a pr on github.
1 parent bb6cd5a commit e0d8b67

5 files changed

Lines changed: 213 additions & 35 deletions

File tree

src/app.js

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ const diagnosticsClearAll = document.getElementById('diagnostics-clear-all')
8282
const diagnosticsComponent = document.getElementById('diagnostics-component')
8383
const diagnosticsStyles = document.getElementById('diagnostics-styles')
8484
const cdnLoading = document.getElementById('cdn-loading')
85+
const appToast = document.getElementById('app-toast')
8586
const previewBgColorInput = document.getElementById('preview-bg-color')
8687
const clearConfirmDialog = document.getElementById('clear-confirm-dialog')
8788
const clearConfirmTitle = document.getElementById('clear-confirm-title')
@@ -100,9 +101,32 @@ let renderRuntime = null
100101
let pendingClearAction = null
101102
let suppressEditorChangeSideEffects = false
102103
let hasAppliedReactModeDefault = false
104+
let appToastDismissTimer = null
103105
const clipboardSupported = Boolean(navigator.clipboard?.writeText)
104106
const aiAssistantFeatureEnabled = isAiAssistantFeatureEnabled()
105107

108+
const showAppToast = message => {
109+
if (!(appToast instanceof HTMLElement)) {
110+
return
111+
}
112+
113+
if (appToastDismissTimer) {
114+
clearTimeout(appToastDismissTimer)
115+
appToastDismissTimer = null
116+
}
117+
118+
appToast.textContent = message
119+
appToast.hidden = false
120+
appToast.dataset.open = 'true'
121+
122+
appToastDismissTimer = setTimeout(() => {
123+
appToast.dataset.open = 'false'
124+
appToastDismissTimer = setTimeout(() => {
125+
appToast.hidden = true
126+
}, 190)
127+
}, 4500)
128+
}
129+
106130
const previewBackground = createPreviewBackgroundController({
107131
previewBgColorInput,
108132
getPreviewHost: () => previewHost,
@@ -651,6 +675,12 @@ prDrawerController = createGitHubPrDrawer({
651675
confirmBeforeSubmit: options => {
652676
confirmAction(options)
653677
},
678+
onPullRequestOpened: ({ url }) => {
679+
const message = url
680+
? `Pull request opened: ${url}`
681+
: 'Pull request opened successfully.'
682+
showAppToast(message)
683+
},
654684
})
655685

656686
prDrawerController.setToken(githubAiContextState.token)
@@ -1423,6 +1453,10 @@ if (typeof stackedRailMediaQuery.addEventListener === 'function') {
14231453
}
14241454

14251455
window.addEventListener('beforeunload', () => {
1456+
if (appToastDismissTimer) {
1457+
clearTimeout(appToastDismissTimer)
1458+
appToastDismissTimer = null
1459+
}
14261460
clearComponentLintRecheckTimer()
14271461
clearStylesLintRecheckTimer()
14281462
lintDiagnostics.dispose()

src/index.html

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -748,6 +748,8 @@ <h2>Open Pull Request</h2>
748748
</div>
749749
</footer>
750750

751+
<div class="app-toast" id="app-toast" role="status" aria-live="polite" hidden></div>
752+
751753
<div class="cdn-loading" id="cdn-loading" role="status" aria-live="polite">
752754
<div class="cdn-loading-card">
753755
<div class="cdn-loading-spinner" aria-hidden="true"></div>

src/modules/github-api.js

Lines changed: 65 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -645,6 +645,18 @@ const toUtf8Base64 = value => {
645645
return btoa(binary)
646646
}
647647

648+
const isMissingShaForExistingFileError = error => {
649+
if (!(error instanceof Error)) {
650+
return false
651+
}
652+
653+
const message = error.message.toLowerCase()
654+
return (
655+
message.includes('sha') &&
656+
(message.includes('already exists') || message.includes('must be supplied'))
657+
)
658+
}
659+
648660
export const upsertRepositoryFile = async ({
649661
token,
650662
owner,
@@ -655,34 +667,62 @@ export const upsertRepositoryFile = async ({
655667
message,
656668
signal,
657669
}) => {
658-
const existingFile = await getRepositoryFileMetadata({
659-
token,
660-
owner,
661-
repo,
662-
path,
663-
ref: branch,
664-
signal,
665-
})
666-
667670
const encodedPath = encodePathForApi(path)
668671

669-
const response = await requestGitHubJson({
670-
token,
671-
url: `${githubApiBaseUrl}/repos/${owner}/${repo}/contents/${encodedPath}`,
672-
method: 'PUT',
673-
body: {
674-
message,
675-
content: toUtf8Base64(content),
676-
branch,
677-
...(existingFile?.sha ? { sha: existingFile.sha } : {}),
678-
},
679-
signal,
680-
})
672+
const baseBody = {
673+
message,
674+
content: toUtf8Base64(content),
675+
branch,
676+
}
681677

682-
return {
683-
path,
684-
commitSha: typeof response?.commit?.sha === 'string' ? response.commit.sha : null,
685-
created: !existingFile?.sha,
678+
try {
679+
const response = await requestGitHubJson({
680+
token,
681+
url: `${githubApiBaseUrl}/repos/${owner}/${repo}/contents/${encodedPath}`,
682+
method: 'PUT',
683+
body: baseBody,
684+
signal,
685+
})
686+
687+
return {
688+
path,
689+
commitSha: typeof response?.commit?.sha === 'string' ? response.commit.sha : null,
690+
created: true,
691+
}
692+
} catch (error) {
693+
if (!isMissingShaForExistingFileError(error)) {
694+
throw error
695+
}
696+
697+
const existingFile = await getRepositoryFileMetadata({
698+
token,
699+
owner,
700+
repo,
701+
path,
702+
ref: branch,
703+
signal,
704+
})
705+
706+
if (!existingFile?.sha) {
707+
throw error
708+
}
709+
710+
const response = await requestGitHubJson({
711+
token,
712+
url: `${githubApiBaseUrl}/repos/${owner}/${repo}/contents/${encodedPath}`,
713+
method: 'PUT',
714+
body: {
715+
...baseBody,
716+
sha: existingFile.sha,
717+
},
718+
signal,
719+
})
720+
721+
return {
722+
path,
723+
commitSha: typeof response?.commit?.sha === 'string' ? response.commit.sha : null,
724+
created: false,
725+
}
686726
}
687727
}
688728

src/modules/github-pr-drawer.js

Lines changed: 76 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,18 @@ const saveRepositoryPrConfig = ({ repositoryFullName, config }) => {
4040
}
4141
}
4242

43+
const clearRepositoryPrConfig = repositoryFullName => {
44+
if (typeof repositoryFullName !== 'string' || !repositoryFullName.trim()) {
45+
return
46+
}
47+
48+
try {
49+
localStorage.removeItem(`${prConfigStoragePrefix}${repositoryFullName}`)
50+
} catch {
51+
/* noop */
52+
}
53+
}
54+
4355
const toSafeText = value => (typeof value === 'string' ? value.trim() : '')
4456

4557
const normalizeFilePath = value =>
@@ -92,10 +104,38 @@ const sanitizeBranchPart = value => {
92104
.replace(/^[-/.]+|[-/.]+$/g, '')
93105
}
94106

107+
const toUtcBranchStamp = () => {
108+
const now = new Date()
109+
const parts = [
110+
String(now.getUTCFullYear()),
111+
String(now.getUTCMonth() + 1).padStart(2, '0'),
112+
String(now.getUTCDate()).padStart(2, '0'),
113+
String(now.getUTCHours()).padStart(2, '0'),
114+
String(now.getUTCMinutes()).padStart(2, '0'),
115+
String(now.getUTCSeconds()).padStart(2, '0'),
116+
]
117+
118+
return `${parts[0]}${parts[1]}${parts[2]}-${parts[3]}${parts[4]}${parts[5]}`
119+
}
120+
121+
const createBranchEntropySuffix = () => Math.random().toString(36).slice(2, 6)
122+
123+
const isAutoGeneratedHeadBranch = value => {
124+
const branch = sanitizeBranchPart(value)
125+
if (!branch) {
126+
return false
127+
}
128+
129+
return /^develop\/[^/]+\/editor-sync-\d{8}(?:-\d{6})?(?:-[a-z0-9]{4})?(?:-\d+)?$/.test(
130+
branch,
131+
)
132+
}
133+
95134
const createDefaultBranchName = repository => {
96135
const repoName = sanitizeBranchPart(repository?.name ?? '') || 'repo'
97-
const stamp = new Date().toISOString().slice(0, 10).replace(/-/g, '')
98-
return `develop/${repoName}/editor-sync-${stamp}`
136+
const stamp = toUtcBranchStamp()
137+
const entropy = createBranchEntropySuffix()
138+
return `develop/${repoName}/editor-sync-${stamp}-${entropy}`
99139
}
100140

101141
const toDefaultPrTitle = repository => {
@@ -156,6 +196,7 @@ export const createGitHubPrDrawer = ({
156196
getStylesSource,
157197
getDrawerSide,
158198
confirmBeforeSubmit,
199+
onPullRequestOpened,
159200
}) => {
160201
if (!featureEnabled) {
161202
toggleButton?.setAttribute('hidden', '')
@@ -174,6 +215,11 @@ export const createGitHubPrDrawer = ({
174215
let open = false
175216
let submitting = false
176217
let pendingAbortController = null
218+
let resetOnNextOpen = false
219+
const submitButtonDefaultLabel =
220+
submitButton instanceof HTMLButtonElement && toSafeText(submitButton.textContent)
221+
? toSafeText(submitButton.textContent)
222+
: 'Open PR'
177223

178224
const setStatus = (text, level = 'neutral') => {
179225
if (!statusNode) {
@@ -190,6 +236,8 @@ export const createGitHubPrDrawer = ({
190236
if (submitButton instanceof HTMLButtonElement) {
191237
submitButton.disabled = isPending
192238
submitButton.setAttribute('aria-busy', isPending ? 'true' : 'false')
239+
submitButton.classList.toggle('render-button--loading', isPending)
240+
submitButton.textContent = isPending ? 'Opening PR...' : submitButtonDefaultLabel
193241
}
194242

195243
for (const input of [
@@ -266,10 +314,10 @@ export const createGitHubPrDrawer = ({
266314
repositorySelect.value = selectedFullName
267315
}
268316

269-
const syncFormForRepository = ({ resetBranch = false } = {}) => {
317+
const syncFormForRepository = ({ resetBranch = false, resetAll = false } = {}) => {
270318
const repository = getSelectedRepositoryObject()
271319
const repositoryFullName = getRepositoryFullName(repository)
272-
const savedConfig = readRepositoryPrConfig(repositoryFullName)
320+
const savedConfig = resetAll ? {} : readRepositoryPrConfig(repositoryFullName)
273321

274322
const componentFilePath =
275323
typeof savedConfig.componentFilePath === 'string' && savedConfig.componentFilePath
@@ -298,21 +346,24 @@ export const createGitHubPrDrawer = ({
298346
}
299347

300348
if (headBranchInput instanceof HTMLInputElement) {
301-
if (resetBranch || !toSafeText(headBranchInput.value)) {
349+
if (resetAll || resetBranch || !toSafeText(headBranchInput.value)) {
350+
const savedHeadBranch = sanitizeBranchPart(savedConfig.headBranch)
302351
headBranchInput.value =
303-
toSafeText(savedConfig.headBranch) || createDefaultBranchName(repository)
352+
savedHeadBranch && !isAutoGeneratedHeadBranch(savedHeadBranch)
353+
? savedHeadBranch
354+
: createDefaultBranchName(repository)
304355
}
305356
}
306357

307358
if (prTitleInput instanceof HTMLInputElement) {
308-
if (!toSafeText(prTitleInput.value)) {
359+
if (resetAll || !toSafeText(prTitleInput.value)) {
309360
prTitleInput.value =
310361
toSafeText(savedConfig.prTitle) || toDefaultPrTitle(repository)
311362
}
312363
}
313364

314365
if (prBodyInput instanceof HTMLTextAreaElement) {
315-
if (!toSafeText(prBodyInput.value)) {
366+
if (resetAll || !toSafeText(prBodyInput.value)) {
316367
prBodyInput.value =
317368
typeof savedConfig.prBody === 'string' && savedConfig.prBody
318369
? savedConfig.prBody
@@ -335,7 +386,7 @@ export const createGitHubPrDrawer = ({
335386
componentFilePath: values.componentFilePath,
336387
stylesFilePath: values.stylesFilePath,
337388
baseBranch: values.baseBranch,
338-
headBranch: values.headBranch,
389+
headBranch: isAutoGeneratedHeadBranch(values.headBranch) ? '' : values.headBranch,
339390
prTitle: values.prTitle,
340391
prBody: values.prBody,
341392
},
@@ -364,7 +415,14 @@ export const createGitHubPrDrawer = ({
364415
drawer.toggleAttribute('hidden', !open)
365416

366417
if (open) {
367-
syncRepositories()
418+
const repositories = getWritableRepositories?.() ?? []
419+
const selectedRepository = getSelectedRepositoryObject()
420+
syncRepositorySelect({ repositories, selectedRepository })
421+
syncFormForRepository({
422+
resetAll: resetOnNextOpen,
423+
resetBranch: resetOnNextOpen,
424+
})
425+
resetOnNextOpen = false
368426
repositorySelect?.focus()
369427
}
370428
}
@@ -455,6 +513,12 @@ export const createGitHubPrDrawer = ({
455513
url ? `Pull request opened: ${url}` : 'Pull request opened successfully.',
456514
'ok',
457515
)
516+
onPullRequestOpened?.({
517+
url,
518+
pullRequestNumber: result.pullRequest.number,
519+
})
520+
clearRepositoryPrConfig(repositoryLabel)
521+
resetOnNextOpen = true
458522
setOpen(false)
459523
})
460524
.catch(error => {
@@ -465,6 +529,8 @@ export const createGitHubPrDrawer = ({
465529
const message =
466530
error instanceof Error ? error.message : 'Failed to open pull request.'
467531
setStatus(`Open PR failed: ${message}`, 'error')
532+
clearRepositoryPrConfig(repositoryLabel)
533+
resetOnNextOpen = true
468534
})
469535
.finally(() => {
470536
if (pendingAbortController === abortController) {

0 commit comments

Comments
 (0)