diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts index 536c7ddccbf..04499d12da7 100644 --- a/packages/shared/src/index.ts +++ b/packages/shared/src/index.ts @@ -1220,6 +1220,7 @@ export type { IssueDocument, IssueDocumentSummary, DocumentRevision, + ExactDocumentRevision, DocumentFormat, LegacyPlanDocument, IssueAttachment, diff --git a/packages/shared/src/types/index.ts b/packages/shared/src/types/index.ts index 884c44fd39c..4aa18840240 100644 --- a/packages/shared/src/types/index.ts +++ b/packages/shared/src/types/index.ts @@ -820,6 +820,7 @@ export type { IssueDocument, IssueDocumentSummary, DocumentRevision, + ExactDocumentRevision, DocumentFormat, LegacyPlanDocument, IssueAncestor, diff --git a/packages/shared/src/types/issue.ts b/packages/shared/src/types/issue.ts index f5a8eaa5493..f78d79c7449 100644 --- a/packages/shared/src/types/issue.ts +++ b/packages/shared/src/types/issue.ts @@ -214,6 +214,12 @@ export interface DocumentRevision { createdAt: Date; } +export interface ExactDocumentRevision extends DocumentRevision { + latestRevisionId: string | null; + latestRevisionNumber: number; + isLatest: boolean; +} + export interface LegacyPlanDocument { key: "plan"; body: string; diff --git a/server/src/__tests__/documents-service.test.ts b/server/src/__tests__/documents-service.test.ts index bab96aba79f..37909508ec7 100644 --- a/server/src/__tests__/documents-service.test.ts +++ b/server/src/__tests__/documents-service.test.ts @@ -109,6 +109,9 @@ describeEmbeddedPostgres("documentService system issue documents", () => { const pinned = await svc.getIssueDocumentRevision(issueId, "plan", original!.latestRevisionId!); expect(pinned?.body).toBe("# Plan"); expect(pinned?.id).toBe(original!.latestRevisionId); + const current = await svc.getIssueDocumentByKey(issueId, "plan"); + expect(pinned).toMatchObject({ isLatest: false, latestRevisionId: current!.latestRevisionId, latestRevisionNumber: 2 }); + expect(await svc.getIssueDocumentRevision(issueId, "plan", current!.latestRevisionId!)).toMatchObject({ isLatest: true, revisionNumber: 2, body: "# Newer plan" }); expect(await svc.getIssueDocumentRevision(issueId, "other-document", original!.latestRevisionId!)).toBeNull(); expect(await svc.getIssueDocumentRevision(randomUUID(), "plan", original!.latestRevisionId!)).toBeNull(); expect(await svc.getIssueDocumentRevision(issueId, "plan", randomUUID())).toBeNull(); diff --git a/server/src/routes/openapi.ts b/server/src/routes/openapi.ts index 405bfa1f99f..82c03636034 100644 --- a/server/src/routes/openapi.ts +++ b/server/src/routes/openapi.ts @@ -4625,7 +4625,7 @@ registry.registerPath({ tags: ["issues"], summary: "Get an exact issue document revision", description: - "Available to board users and agents with current issue read access. Returns the specified revision only when its document is linked to this issue under the given key. Document keys are trimmed and lowercased before validation. This read does not restore or modify the document.", + "Available to board users and agents with current issue read access. Returns the specified revision only when its document is linked to this issue under the given key. Document keys are trimmed and lowercased before validation. The response also carries the document's current latestRevisionId and latestRevisionNumber, and isLatest says whether this revision is still current; to read the current text, use GET /api/issues/{id}/documents/{key}. This read does not restore or modify the document.", request: { params: z.object({ id: z.string(), diff --git a/server/src/services/documents.ts b/server/src/services/documents.ts index 7c81d075354..fb8da036dad 100644 --- a/server/src/services/documents.ts +++ b/server/src/services/documents.ts @@ -181,11 +181,12 @@ export function documentService(db: Db) { format: documentRevisions.format, body: documentRevisions.body, changeSummary: documentRevisions.changeSummary, createdByAgentId: documentRevisions.createdByAgentId, createdByUserId: documentRevisions.createdByUserId, createdByRunId: documentRevisions.createdByRunId, createdAt: documentRevisions.createdAt, + latestRevisionId: documents.latestRevisionId, latestRevisionNumber: documents.latestRevisionNumber, }).from(issueDocuments) .innerJoin(documents, eq(issueDocuments.documentId, documents.id)) .innerJoin(documentRevisions, and(eq(documentRevisions.documentId, documents.id), eq(documentRevisions.companyId, documents.companyId))) .where(and(eq(issueDocuments.issueId, issueId), eq(issueDocuments.key, key), eq(documentRevisions.id, revisionId))).limit(1); - return revision ?? null; + return revision ? { ...revision, isLatest: revision.latestRevisionId === revision.id } : null; }, listIssueDocumentRevisions: async (issueId: string, rawKey: string) => { diff --git a/ui/src/api/issues.ts b/ui/src/api/issues.ts index 178c0a521a9..803d206ef01 100644 --- a/ui/src/api/issues.ts +++ b/ui/src/api/issues.ts @@ -11,6 +11,7 @@ import type { InboxMineSnapshot, CreateIssueTreeHold, DocumentRevision, + ExactDocumentRevision, FeedbackTargetType, FeedbackTrace, FeedbackVote, @@ -747,7 +748,7 @@ export const issuesApi = { `/issues/${id}/documents/${encodeURIComponent(key)}/revisions`, ), getDocumentRevision: (id: string, key: string, revisionId: string) => - api.get(`/issues/${id}/documents/${encodeURIComponent(key)}/revisions/${encodeURIComponent(revisionId)}`), + api.get(`/issues/${id}/documents/${encodeURIComponent(key)}/revisions/${encodeURIComponent(revisionId)}`), restoreDocumentRevision: (id: string, key: string, revisionId: string) => api.post( `/issues/${id}/documents/${encodeURIComponent(key)}/revisions/${revisionId}/restore`,