orca/src/shared/issue-link-input.ts

76 lines
2.7 KiB
TypeScript

import { parseGitHubIssueOrPRLink } from './github-links'
import { parseLinearIssueInput } from './linear-links'
import type { WorkspaceSourceProvider } from './new-workspace/workspace-source'
// Why: narrows the canonical provider union instead of minting a parallel one,
// so adding Jira here is a one-entry change rather than a new axis.
export const ISSUE_LINK_PROVIDERS = [
'github',
'linear'
] as const satisfies readonly WorkspaceSourceProvider[]
export type IssueLinkProvider = (typeof ISSUE_LINK_PROVIDERS)[number]
export function isIssueLinkProvider(value: unknown): value is IssueLinkProvider {
return ISSUE_LINK_PROVIDERS.includes(value as IssueLinkProvider)
}
/** URL input only. Linear and Jira issue keys are byte-identical in shape, so a
* bare `STA-335` can never decide a provider — it would override the chip. */
export function getIssueLinkProviderFromUrl(input: string): IssueLinkProvider | null {
const trimmed = input.trim()
if (!/^https?:\/\//i.test(trimmed)) {
return null
}
// Why: a URL is decisive only if it names that provider's *issue*. A GitHub
// pull URL is a valid GitHub link but not an issue, and must not flip the
// provider into a state where the field then refuses to save it.
if (parseGitHubIssueOrPRLink(trimmed)?.type === 'issue') {
return 'github'
}
if (parseLinearIssueInput(trimmed)) {
return 'linear'
}
return null
}
export type ParsedIssueLinkInput =
| { provider: 'github'; number: number }
| { provider: 'linear'; identifier: string; organizationUrlKey?: string }
/**
* Single parse shared by the dialog's save gate and its payload builder, so a
* value that enables Save is always the value that gets written.
*/
export function parseIssueLinkInput(
input: string,
provider: IssueLinkProvider
): ParsedIssueLinkInput | null {
const trimmed = input.trim()
if (!trimmed) {
return null
}
if (provider === 'linear') {
const parsed = parseLinearIssueInput(trimmed)
return parsed ? { provider: 'linear', ...parsed } : null
}
const link = parseGitHubIssueOrPRLink(trimmed)
if (link) {
// Why: issue and PR numbers live in separate GitHub namespaces for refs; a
// pull URL pasted here must not silently become an issue link.
return link.type === 'issue' ? { provider: 'github', number: link.number } : null
}
const numeric = trimmed.startsWith('#') ? trimmed.slice(1) : trimmed
if (!/^\d+$/.test(numeric)) {
return null
}
const parsed = Number.parseInt(numeric, 10)
// Why: `/^\d+$/` happily accepts 400 digits, which parseInt turns into
// Infinity — Save would enable and JSON.stringify would persist `null`.
return Number.isSafeInteger(parsed) && parsed > 0 ? { provider: 'github', number: parsed } : null
}