Feature: document file versions (#12061)

This commit is contained in:
shamoon authored and GitHub committed 2026-02-26 16:46:54 +00:00
1 parent c9278f88ca
commit ceee769e26
35 files changed
+4068 -365

No files matched your search

@@ -165,6 +165,14 @@ describe(`DocumentService`, () => {
expect(req.request.method).toEqual('GET')
})
it('should call appropriate api endpoint for versioned metadata', () => {
subscription = service.getMetadata(documents[0].id, 123).subscribe()
const req = httpTestingController.expectOne(
`${environment.apiBaseUrl}${endpoint}/${documents[0].id}/metadata/?version=123`
)
expect(req.request.method).toEqual('GET')
})
it('should call appropriate api endpoint for getting selection data', () => {
const ids = [documents[0].id]
subscription = service.getSelectionData(ids).subscribe()
@@ -233,11 +241,22 @@ describe(`DocumentService`, () => {
)
})
it('should return the correct preview URL for a specific version', () => {
const url = service.getPreviewUrl(documents[0].id, false, 123)
expect(url).toEqual(
`${environment.apiBaseUrl}${endpoint}/${documents[0].id}/preview/?version=123`
)
})
it('should return the correct thumb URL for a single document', () => {
let url = service.getThumbUrl(documents[0].id)
expect(url).toEqual(
`${environment.apiBaseUrl}${endpoint}/${documents[0].id}/thumb/`
)
url = service.getThumbUrl(documents[0].id, 123)
expect(url).toEqual(
`${environment.apiBaseUrl}${endpoint}/${documents[0].id}/thumb/?version=123`
)
})
it('should return the correct download URL for a single document', () => {
@@ -249,6 +268,22 @@ describe(`DocumentService`, () => {
expect(url).toEqual(
`${environment.apiBaseUrl}${endpoint}/${documents[0].id}/download/?original=true`
)
url = service.getDownloadUrl(documents[0].id, false, 123)
expect(url).toEqual(
`${environment.apiBaseUrl}${endpoint}/${documents[0].id}/download/?version=123`
)
url = service.getDownloadUrl(documents[0].id, true, 123)
expect(url).toEqual(
`${environment.apiBaseUrl}${endpoint}/${documents[0].id}/download/?original=true&version=123`
)
})
it('should pass optional get params for version and fields', () => {
subscription = service.get(documents[0].id, 123, 'content').subscribe()
const req = httpTestingController.expectOne(
`${environment.apiBaseUrl}${endpoint}/${documents[0].id}/?full_perms=true&version=123&fields=content`
)
expect(req.request.method).toEqual('GET')
})
it('should set search query', () => {
@@ -283,12 +318,77 @@ describe(`DocumentService`, () => {
expect(req.request.body.remove_inbox_tags).toEqual(true)
})
it('should pass selected version to patch when provided', () => {
subscription = service.patch(documents[0], 123).subscribe()
const req = httpTestingController.expectOne(
`${environment.apiBaseUrl}${endpoint}/${documents[0].id}/?version=123`
)
expect(req.request.method).toEqual('PATCH')
})
it('should call appropriate api endpoint for getting audit log', () => {
subscription = service.getHistory(documents[0].id).subscribe()
const req = httpTestingController.expectOne(
`${environment.apiBaseUrl}${endpoint}/${documents[0].id}/history/`
)
})
it('should call appropriate api endpoint for getting root document id', () => {
subscription = service.getRootId(documents[0].id).subscribe()
const req = httpTestingController.expectOne(
`${environment.apiBaseUrl}${endpoint}/${documents[0].id}/root/`
)
expect(req.request.method).toEqual('GET')
req.flush({ root_id: documents[0].id })
})
it('should call appropriate api endpoint for getting document versions', () => {
subscription = service.getVersions(documents[0].id).subscribe()
const req = httpTestingController.expectOne(
`${environment.apiBaseUrl}${endpoint}/${documents[0].id}/?fields=id,versions`
)
expect(req.request.method).toEqual('GET')
})
it('should call appropriate api endpoint for deleting a document version', () => {
subscription = service.deleteVersion(documents[0].id, 10).subscribe()
const req = httpTestingController.expectOne(
`${environment.apiBaseUrl}${endpoint}/${documents[0].id}/versions/10/`
)
expect(req.request.method).toEqual('DELETE')
req.flush({ result: 'OK', current_version_id: documents[0].id })
})
it('should call appropriate api endpoint for updating a document version label', () => {
subscription = service
.updateVersionLabel(documents[0].id, 10, 'Updated label')
.subscribe()
const req = httpTestingController.expectOne(
`${environment.apiBaseUrl}${endpoint}/${documents[0].id}/versions/10/`
)
expect(req.request.method).toEqual('PATCH')
expect(req.request.body).toEqual({ version_label: 'Updated label' })
req.flush({ id: 10, version_label: 'Updated label', is_root: false })
})
it('should call appropriate api endpoint for uploading a new version', () => {
const file = new File(['hello'], 'test.pdf', { type: 'application/pdf' })
subscription = service
.uploadVersion(documents[0].id, file, 'Label')
.subscribe()
const req = httpTestingController.expectOne(
`${environment.apiBaseUrl}${endpoint}/${documents[0].id}/update_version/`
)
expect(req.request.method).toEqual('POST')
expect(req.request.body).toBeInstanceOf(FormData)
const body = req.request.body as FormData
expect(body.get('version_label')).toEqual('Label')
expect(body.get('document')).toBeInstanceOf(File)
req.flush('task-id')
})
})
it('should construct sort fields respecting permissions', () => {
@@ -7,6 +7,7 @@ import {
DOCUMENT_SORT_FIELDS,
DOCUMENT_SORT_FIELDS_FULLTEXT,
Document,
DocumentVersionInfo,
} from 'src/app/data/document'
import { DocumentMetadata } from 'src/app/data/document-metadata'
import { DocumentSuggestions } from 'src/app/data/document-suggestions'
@@ -155,44 +156,119 @@ export class DocumentService extends AbstractPaperlessService<Document> {
}).pipe(map((response) => response.results.map((doc) => doc.id)))
}
get(id: number): Observable<Document> {
get(
id: number,
versionID: number = null,
fields: string = null
): Observable<Document> {
const params: { full_perms: boolean; version?: string; fields?: string } = {
full_perms: true,
}
if (versionID) {
params.version = versionID.toString()
}
if (fields) {
params.fields = fields
}
return this.http.get<Document>(this.getResourceUrl(id), {
params: {
full_perms: true,
},
params,
})
}
getPreviewUrl(id: number, original: boolean = false): string {
getPreviewUrl(
id: number,
original: boolean = false,
versionID: number = null
): string {
let url = new URL(this.getResourceUrl(id, 'preview'))
if (this._searchQuery) url.hash = `#search="${this.searchQuery}"`
if (original) {
url.searchParams.append('original', 'true')
}
if (versionID) {
url.searchParams.append('version', versionID.toString())
}
return url.toString()
}
getThumbUrl(id: number): string {
return this.getResourceUrl(id, 'thumb')
getThumbUrl(id: number, versionID: number = null): string {
let url = new URL(this.getResourceUrl(id, 'thumb'))
if (versionID) {
url.searchParams.append('version', versionID.toString())
}
return url.toString()
}
getDownloadUrl(id: number, original: boolean = false): string {
let url = this.getResourceUrl(id, 'download')
getDownloadUrl(
id: number,
original: boolean = false,
versionID: number = null
): string {
let url = new URL(this.getResourceUrl(id, 'download'))
if (original) {
url += '?original=true'
url.searchParams.append('original', 'true')
}
return url
if (versionID) {
url.searchParams.append('version', versionID.toString())
}
return url.toString()
}
uploadVersion(documentId: number, file: File, versionLabel?: string) {
const formData = new FormData()
formData.append('document', file, file.name)
if (versionLabel) {
formData.append('version_label', versionLabel)
}
return this.http.post<string>(
this.getResourceUrl(documentId, 'update_version'),
formData
)
}
getVersions(documentId: number): Observable<Document> {
return this.http.get<Document>(this.getResourceUrl(documentId), {
params: {
fields: 'id,versions',
},
})
}
getRootId(documentId: number) {
return this.http.get<{ root_id: number }>(
this.getResourceUrl(documentId, 'root')
)
}
deleteVersion(rootDocumentId: number, versionId: number) {
return this.http.delete<{ result: string; current_version_id: number }>(
this.getResourceUrl(rootDocumentId, `versions/${versionId}`)
)
}
updateVersionLabel(
rootDocumentId: number,
versionId: number,
versionLabel: string | null
): Observable<DocumentVersionInfo> {
return this.http.patch<DocumentVersionInfo>(
this.getResourceUrl(rootDocumentId, `versions/${versionId}`),
{ version_label: versionLabel }
)
}
getNextAsn(): Observable<number> {
return this.http.get<number>(this.getResourceUrl(null, 'next_asn'))
}
patch(o: Document): Observable<Document> {
patch(o: Document, versionID: number = null): Observable<Document> {
o.remove_inbox_tags = !!this.settingsService.get(
SETTINGS_KEYS.DOCUMENT_EDITING_REMOVE_INBOX_TAGS
)
return super.patch(o)
this.clearCache()
return this.http.patch<Document>(this.getResourceUrl(o.id), o, {
params: versionID ? { version: versionID.toString() } : {},
})
}
uploadDocument(formData) {
@@ -203,8 +279,15 @@ export class DocumentService extends AbstractPaperlessService<Document> {
)
}
getMetadata(id: number): Observable<DocumentMetadata> {
return this.http.get<DocumentMetadata>(this.getResourceUrl(id, 'metadata'))
getMetadata(
id: number,
versionID: number = null
): Observable<DocumentMetadata> {
let url = new URL(this.getResourceUrl(id, 'metadata'))
if (versionID) {
url.searchParams.append('version', versionID.toString())
}
return this.http.get<DocumentMetadata>(url.toString())
}
bulkEdit(ids: number[], method: string, args: any) {
@@ -89,6 +89,13 @@ export class FileStatus {
}
}
export enum UploadState {
Idle = 'idle',
Uploading = 'uploading',
Processing = 'processing',
Failed = 'failed',
}
@Injectable({
providedIn: 'root',
})