Files
superwork/vscode/src/youtrack/issueLoader.ts
T

82 lines
3.1 KiB
TypeScript

/**
* Loads YouTrack-sourced issues into the panel's `Issue` shape — the YouTrack
* counterpart of the [forge issue loader](vscode/src/forge/issueLoader.ts).
*
* Returns `[]` (not an error) when YouTrack isn't configured, so the panel can
* always call this alongside the forge loader and just merge the results.
*/
import type { ExtensionContext } from 'vscode'
import type { Issue } from '../issues/types'
import type { YouTrackIssue } from './api'
import { workspace } from 'vscode'
import { getYouTrackToken } from '../auth/secrets'
import { latestState } from '../issues/stateComment'
import { issueStateFields } from '../issues/stateFields'
import { getSettings } from '../settings/store'
import { issueHtmlUrl, listIssues } from './api'
import { readImportedIds } from './importStore'
/** Host part of the configured base URL — the SecretStorage token key. */
export function youtrackHost(baseUrl: string): string {
try {
return new URL(baseUrl).host
}
catch {
return baseUrl.replace(/^https?:\/\//, '').replace(/\/.*$/, '')
}
}
/**
* Offset added to a YouTrack issue's numeric id so its board `number` never
* collides with a forge issue number (which is the routing key for
* webview↔extension messages and the `number → IssueRef` map).
* ponytail: assumes forge issue numbers < 1,000,000 and a single mirrored project
* (readable ids are unique within one project). Widen / hash if either breaks.
*/
const YOUTRACK_NUMBER_OFFSET = 1_000_000
/** `LXF-12` → `1_000_012`; falls back to the offset when there's no numeric tail. */
function syntheticNumber(idReadable: string): number {
const m = idReadable.match(/-(\d+)$/)
return YOUTRACK_NUMBER_OFFSET + (m ? Number(m[1]) : 0)
}
function toIssue(baseUrl: string, it: YouTrackIssue): Issue {
const { column, ...fields } = issueStateFields(latestState(it.comments.map(comment => comment.text)) ?? {})
return {
id: `youtrack:${it.idReadable}`,
externalId: it.idReadable,
source: 'youtrack',
number: syntheticNumber(it.idReadable),
title: it.summary,
column: column ?? (it.resolved != null ? 'done' : 'todo'),
htmlUrl: issueHtmlUrl(baseUrl, it.idReadable),
attachments: it.attachments,
...fields,
}
}
export async function loadYouTrackIssues(ctx: ExtensionContext): Promise<Issue[]> {
const settings = getSettings(ctx)
const baseUrl = settings.youtrackBaseUrl.trim()
const project = settings.youtrackProjectShortName.trim()
if (!baseUrl || !project)
return []
const token = await getYouTrackToken(ctx, youtrackHost(baseUrl))
if (!token)
return []
const workspaceRoot = workspace.workspaceFolders?.[0]?.uri.fsPath
if (!workspaceRoot)
return []
// 只镜像用户主动勾选导入的工单(见 importStore),不再加载即全量同步。
// import 集为空 ⇒ 返回 []。
const importedSet = new Set(await readImportedIds(workspaceRoot, project))
if (importedSet.size === 0)
return []
const raw = await listIssues({ baseUrl, token }, project)
return raw
.filter(it => importedSet.has(it.idReadable))
.map(it => toIssue(baseUrl, it))
}