Phase 11: version PDF (FR-15)
- เลข version ใหม่: เว็บ-generate = v0 ฉบับตั้งต้น (ไม่มี PDF), add-in save (Start Versioning + Save & Version) = v1+ มี PDF เสมอ - PDF ผลิตจาก Word จริงผ่าน task pane เท่านั้น (getFileAsync Pdf + runtime guard, manifest ไม่เพิ่ม requirement); capture/upload เป็น best-effort — docx ผ่านแม้ PDF ล้ม + MessageBar เตือนตาม hasPdf - Backend: DocumentVersion.PdfObjectKey (nullable, migration AddVersionPdf) + sibling .pdf ใน bucket docx-versions · multipart part pdf (ผิด type → 400) · /download + /download-link รับ ?format=pdf (ไม่มี → 404) · presigned ฝังชื่อไฟล์ไทย + _vN (RFC 5987) - Web: เปิดไฟล์แทน blob-download — PDF แท็บใหม่, docx ผ่าน ms-word:ofe|u| พร้อม heuristic fallback ลิงก์สำรอง (useVersionOpen, versionDownload) · ปุ่ม "เปิดใน Word" ที่หน้า success ของฟอร์มสร้างเอกสาร - Spec v7 + CONTEXT.md + ADR 0001 + แผน docs/plans/version-pdf.md - Automated QA: backend 68 / add-in 63 / web 55 ผ่าน · manual acceptance ผ่านบน Word for Mac (2026-09-08) รวม fidelity + track changes — ค้างเฉพาะ Windows / Word on the web Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
parent
618328ffa7
commit
39d610a781
39 changed files with 1387 additions and 212 deletions
75
web/app/composables/useVersionOpen.ts
Normal file
75
web/app/composables/useVersionOpen.ts
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
import { onBeforeUnmount, ref } from 'vue'
|
||||
import { versionDownloadPath, wordOpenUrl, type VersionFormat } from '../utils/versionDownload'
|
||||
|
||||
/** Shared by history and generate success. Links stay available even if the focus heuristic fires. */
|
||||
export function useVersionOpen() {
|
||||
const { apiJson } = useApi()
|
||||
const openingVersionId = ref<string | null>(null)
|
||||
const downloadError = ref<string | null>(null)
|
||||
const fallbackUrl = ref<string | null>(null)
|
||||
const openNotice = ref<string | null>(null)
|
||||
let cleanup = () => {}
|
||||
let generation = 0
|
||||
let pendingTab: Window | null = null
|
||||
|
||||
function resetOpen() {
|
||||
generation++
|
||||
pendingTab?.close()
|
||||
pendingTab = null
|
||||
cleanup()
|
||||
openingVersionId.value = null
|
||||
downloadError.value = null
|
||||
fallbackUrl.value = null
|
||||
openNotice.value = null
|
||||
}
|
||||
|
||||
async function openVersion(documentId: string, versionId: string, format: VersionFormat = 'docx') {
|
||||
if (openingVersionId.value) return
|
||||
resetOpen()
|
||||
const current = generation
|
||||
openingVersionId.value = versionId
|
||||
// Reserve a tab during the user gesture, before the authenticated request loses activation.
|
||||
let tab: Window | null = null
|
||||
try {
|
||||
tab = format === 'pdf' ? window.open('about:blank', '_blank') : null
|
||||
if (tab) tab.opener = null
|
||||
pendingTab = tab
|
||||
const link = await apiJson<{ url: string; expiresAt: string }>(versionDownloadPath(documentId, versionId, format))
|
||||
if (current !== generation) { tab?.close(); return }
|
||||
if (!link) throw new Error('ขอลิงก์เปิดไฟล์ไม่สำเร็จ')
|
||||
fallbackUrl.value = link.url
|
||||
if (format === 'pdf') {
|
||||
if (tab && !tab.closed) {
|
||||
tab.location.replace(link.url)
|
||||
pendingTab = null
|
||||
}
|
||||
else openNotice.value = 'เบราว์เซอร์บล็อกแท็บใหม่ กรุณากดลิงก์เปิดไฟล์ด้านล่าง'
|
||||
} else {
|
||||
let switched = false
|
||||
const onBlur = () => { switched = true }
|
||||
const onVisibility = () => { if (document.hidden) switched = true }
|
||||
window.addEventListener('blur', onBlur)
|
||||
document.addEventListener('visibilitychange', onVisibility)
|
||||
const timer = window.setTimeout(() => {
|
||||
if (!switched) openNotice.value = 'หาก Word ไม่เปิด กรุณาดาวน์โหลดไฟล์แล้วเปิดใน Word'
|
||||
cleanup()
|
||||
}, 1500)
|
||||
cleanup = () => {
|
||||
window.clearTimeout(timer)
|
||||
window.removeEventListener('blur', onBlur)
|
||||
document.removeEventListener('visibilitychange', onVisibility)
|
||||
}
|
||||
window.location.assign(wordOpenUrl(link.url))
|
||||
}
|
||||
} catch (err) {
|
||||
tab?.close()
|
||||
if (pendingTab === tab) pendingTab = null
|
||||
if (current === generation) downloadError.value = err instanceof Error ? err.message : 'เปิดไฟล์ไม่สำเร็จ'
|
||||
} finally {
|
||||
if (current === generation) openingVersionId.value = null
|
||||
}
|
||||
}
|
||||
|
||||
onBeforeUnmount(resetOpen)
|
||||
return { openingVersionId, downloadError, fallbackUrl, openNotice, openVersion, resetOpen }
|
||||
}
|
||||
|
|
@ -1,10 +1,11 @@
|
|||
<script setup lang="ts">
|
||||
/** FR-12 UI — detail ของเอกสาร: ค่าล่าสุดของทุก field + version history (ขยายแถวเห็นค่าตอนนั้น)
|
||||
* + ปุ่ม re-generate: พาไปฟอร์มสร้างเอกสารแบบ prefill ค่าปัจจุบัน → ได้เอกสารใหม่ v1 (ไม่แตะเอกสารเดิม) */
|
||||
* + ปุ่ม re-generate: พาไปฟอร์มสร้างเอกสารแบบ prefill ค่าปัจจุบัน → ได้เอกสารใหม่ v0 (ไม่แตะเอกสารเดิม) */
|
||||
import type { DocumentDetail, VersionListItem } from '~/types/api'
|
||||
|
||||
const route = useRoute()
|
||||
const { apiJson, downloadFile } = useApi()
|
||||
const { apiJson } = useApi()
|
||||
const { openingVersionId, downloadError, fallbackUrl, openNotice, openVersion } = useVersionOpen()
|
||||
|
||||
const documentId = computed(() => String(route.params.documentId))
|
||||
|
||||
|
|
@ -14,8 +15,6 @@ const loading = ref(true)
|
|||
const loadError = ref<string | null>(null)
|
||||
|
||||
const expandedVersionId = ref<string | null>(null)
|
||||
const downloadingVersionId = ref<string | null>(null)
|
||||
const downloadError = ref<string | null>(null)
|
||||
|
||||
const fieldOrderKeys = computed(() => detail.value?.fields.filter((f) => f.label !== null).map((f) => f.key) ?? [])
|
||||
|
||||
|
|
@ -52,19 +51,6 @@ function regenerate() {
|
|||
navigateTo(`/documents/create/${detail.value.templateId}?from=${detail.value.id}`)
|
||||
}
|
||||
|
||||
async function download(v: VersionListItem) {
|
||||
downloadingVersionId.value = v.versionId
|
||||
downloadError.value = null
|
||||
try {
|
||||
const base = (detail.value?.fileName ?? 'document').replace(/\.docx$/i, '')
|
||||
await downloadFile(`/api/documents/${documentId.value}/versions/${v.versionId}/download`, `${base}_v${v.versionNumber}.docx`)
|
||||
} catch (err) {
|
||||
downloadError.value = err instanceof Error ? err.message : 'ดาวน์โหลดไม่สำเร็จ'
|
||||
} finally {
|
||||
downloadingVersionId.value = null
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(load)
|
||||
</script>
|
||||
|
||||
|
|
@ -190,12 +176,14 @@ onMounted(load)
|
|||
<div class="flex shrink-0 flex-col items-end gap-2">
|
||||
<button
|
||||
type="button"
|
||||
:disabled="downloadingVersionId === v.versionId"
|
||||
:disabled="openingVersionId !== null"
|
||||
class="btn btn-secondary btn-xs"
|
||||
@click.stop="download(v)"
|
||||
@click.stop="openVersion(documentId, v.versionId)"
|
||||
>
|
||||
{{ downloadingVersionId === v.versionId ? '…' : '⬇ ดาวน์โหลด' }}
|
||||
{{ openingVersionId !== null ? '…' : 'เปิดใน Word' }}
|
||||
</button>
|
||||
<button v-if="v.hasPdf" type="button" class="btn btn-secondary btn-xs"
|
||||
:disabled="openingVersionId !== null" @click.stop="openVersion(documentId, v.versionId, 'pdf')">PDF</button>
|
||||
<button
|
||||
type="button"
|
||||
class="text-xs text-muted"
|
||||
|
|
@ -257,12 +245,14 @@ onMounted(load)
|
|||
<td class="px-4 py-3 text-right whitespace-nowrap">
|
||||
<button
|
||||
type="button"
|
||||
:disabled="downloadingVersionId === v.versionId"
|
||||
:disabled="openingVersionId !== null"
|
||||
class="btn btn-secondary btn-xs"
|
||||
@click.stop="download(v)"
|
||||
@click.stop="openVersion(documentId, v.versionId)"
|
||||
>
|
||||
{{ downloadingVersionId === v.versionId ? '…' : '⬇ ดาวน์โหลด' }}
|
||||
{{ openingVersionId !== null ? '…' : 'เปิดใน Word' }}
|
||||
</button>
|
||||
<button v-if="v.hasPdf" type="button" class="btn btn-secondary btn-xs ml-2"
|
||||
:disabled="openingVersionId !== null" @click.stop="openVersion(documentId, v.versionId, 'pdf')">PDF</button>
|
||||
<span class="ml-2 text-xs text-muted">
|
||||
{{ expandedVersionId === v.versionId ? '▲ ซ่อน fields' : '▼ ดู fields' }}
|
||||
</span>
|
||||
|
|
@ -292,6 +282,11 @@ onMounted(load)
|
|||
</table>
|
||||
</div>
|
||||
</template>
|
||||
<AlertBox v-if="fallbackUrl">
|
||||
<p v-if="openNotice">{{ openNotice }}</p>
|
||||
<a :href="fallbackUrl" target="_blank" rel="noopener noreferrer" class="link">เปิด / ดาวน์โหลดไฟล์สำรอง</a>
|
||||
<p class="text-xs text-muted">ลิงก์มีอายุสั้น หากหมดอายุให้กดเปิดไฟล์อีกครั้ง</p>
|
||||
</AlertBox>
|
||||
<AlertBox v-if="downloadError" type="error">{{ downloadError }}</AlertBox>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
<script setup lang="ts">
|
||||
/** FR-10 UI — dynamic form สร้างเอกสารจาก field definitions ของ template → generate เป็น v1 + ดาวน์โหลด
|
||||
/** FR-10 UI — dynamic form สร้างเอกสารจาก field definitions ของ template → generate เป็น v0 + ดาวน์โหลด
|
||||
* FR-12 — รองรับ re-generate: ?from={documentId} → prefill ค่าล่าสุดของเอกสารนั้นให้แก้ก่อนสร้าง */
|
||||
import type { DocumentDetail, GenerateDocumentResponse, TemplateDetail } from '~/types/api'
|
||||
|
||||
const route = useRoute()
|
||||
const { apiJson, downloadFile } = useApi()
|
||||
const { apiJson } = useApi()
|
||||
const { openingVersionId, downloadError, fallbackUrl, openNotice, openVersion, resetOpen } = useVersionOpen()
|
||||
|
||||
const templateId = computed(() => String(route.params.templateId))
|
||||
const detail = ref<TemplateDetail | null>(null)
|
||||
|
|
@ -23,8 +24,6 @@ const loadError = ref<string | null>(null)
|
|||
const submitting = ref(false)
|
||||
const submitError = ref<string | null>(null)
|
||||
const result = ref<GenerateDocumentResponse | null>(null)
|
||||
const downloading = ref(false)
|
||||
const downloadError = ref<string | null>(null)
|
||||
|
||||
const sortedFields = computed(() => [...(detail.value?.fields ?? [])].sort((a, b) => a.sortOrder - b.sortOrder))
|
||||
|
||||
|
|
@ -98,22 +97,9 @@ async function submit() {
|
|||
}
|
||||
}
|
||||
|
||||
async function download() {
|
||||
if (!result.value) return
|
||||
downloading.value = true
|
||||
downloadError.value = null
|
||||
try {
|
||||
const name = detail.value ? `${detail.value.name}_v${result.value.versionNumber}.docx` : 'document_v1.docx'
|
||||
await downloadFile(result.value.downloadUrl, name)
|
||||
} catch (err) {
|
||||
downloadError.value = err instanceof Error ? err.message : 'ดาวน์โหลดไม่สำเร็จ'
|
||||
} finally {
|
||||
downloading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** กลับไปกรอกฟอร์มใหม่เพื่อสร้างอีกฉบับจาก template เดิม (ถ้าเป็น re-generate → คืนค่า prefill ไม่ใช่ฟอร์มว่าง) */
|
||||
function resetForm() {
|
||||
resetOpen()
|
||||
result.value = null
|
||||
submitError.value = null
|
||||
downloadError.value = null
|
||||
|
|
@ -139,7 +125,7 @@ onMounted(load)
|
|||
</AlertBox>
|
||||
|
||||
<template v-else-if="detail">
|
||||
<!-- ผลลัพธ์: สร้างสำเร็จเป็น v1 แล้ว -->
|
||||
<!-- ผลลัพธ์: สร้างสำเร็จเป็น v0 แล้ว -->
|
||||
<template v-if="result">
|
||||
<AlertBox type="success">
|
||||
สร้างเอกสารสำเร็จ — บันทึกเป็น <strong>version {{ result.versionNumber }}</strong> แล้ว
|
||||
|
|
@ -165,13 +151,18 @@ onMounted(load)
|
|||
</button>
|
||||
<button
|
||||
type="button"
|
||||
:disabled="downloading"
|
||||
:disabled="openingVersionId !== null"
|
||||
class="btn btn-primary w-full sm:w-auto"
|
||||
@click="download"
|
||||
@click="openVersion(result.documentId, result.versionId)"
|
||||
>
|
||||
{{ downloading ? 'กำลังเตรียมไฟล์…' : '⬇ ดาวน์โหลด .docx' }}
|
||||
{{ openingVersionId !== null ? 'กำลังเตรียมไฟล์…' : 'เปิดใน Word' }}
|
||||
</button>
|
||||
</div>
|
||||
<AlertBox v-if="fallbackUrl" class="mt-4">
|
||||
<p v-if="openNotice">{{ openNotice }}</p>
|
||||
<a :href="fallbackUrl" target="_blank" rel="noopener noreferrer" class="link">ดาวน์โหลด .docx สำรอง</a>
|
||||
<p class="text-xs text-muted">ลิงก์มีอายุสั้น หากหมดอายุให้กดเปิดใน Word อีกครั้ง</p>
|
||||
</AlertBox>
|
||||
<AlertBox v-if="downloadError" type="error" class="mt-4">{{ downloadError }}</AlertBox>
|
||||
</div>
|
||||
|
||||
|
|
@ -181,7 +172,7 @@ onMounted(load)
|
|||
<li>เปิดไฟล์ที่ดาวน์โหลดใน Word (Desktop หรือ Online)</li>
|
||||
<li>
|
||||
เปิด task pane ของ add-in <strong>e-Court Documents Versioning</strong> — ระบบจะตรวจพบว่าเป็น
|
||||
tracked document และแสดง <strong>version 1</strong> ทันที โดยไม่ต้องกด Start Versioning
|
||||
tracked document และแสดง <strong>version 0</strong> ทันที โดยไม่ต้องกด Start Versioning
|
||||
</li>
|
||||
<li>แก้ไขเอกสารต่อแล้วกด <strong>Save & Version</strong> เพื่อบันทึก version ถัดไป</li>
|
||||
</ol>
|
||||
|
|
@ -278,7 +269,7 @@ onMounted(load)
|
|||
:disabled="submitting || sortedFields.length === 0 || !detail.isActive"
|
||||
class="btn btn-primary w-full sm:w-auto"
|
||||
>
|
||||
{{ submitting ? 'กำลังสร้างเอกสาร…' : 'สร้างเอกสาร (version 1)' }}
|
||||
{{ submitting ? 'กำลังสร้างเอกสาร…' : 'สร้างเอกสาร (version 0)' }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
|
|
|||
|
|
@ -97,6 +97,7 @@ export interface DocumentDetail {
|
|||
/** item ของ GET /api/documents/{documentId}/versions — fields คือ snapshot ที่ parse แล้ว
|
||||
* (2 รูปแบบ — ผ่าน normalizeSnapshot ก่อนใช้) */
|
||||
export interface VersionListItem {
|
||||
hasPdf: boolean;
|
||||
versionId: string
|
||||
versionNumber: number
|
||||
createdAt: string
|
||||
|
|
|
|||
14
web/app/utils/versionDownload.ts
Normal file
14
web/app/utils/versionDownload.ts
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
export type VersionFormat = 'docx' | 'pdf'
|
||||
|
||||
export function versionFileName(fileName: string, versionNumber: number, format: VersionFormat = 'docx'): string {
|
||||
return `${fileName.replace(/\.docx$/i, '')}_v${versionNumber}.${format}`
|
||||
}
|
||||
|
||||
export function versionDownloadPath(documentId: string, versionId: string, format: VersionFormat = 'docx'): string {
|
||||
return `/api/documents/${documentId}/versions/${versionId}/download-link${format === 'pdf' ? '?format=pdf' : ''}`
|
||||
}
|
||||
|
||||
/** Office URI Schemes: preserve the signed URL, including its existing percent escapes. */
|
||||
export function wordOpenUrl(presignedUrl: string): string {
|
||||
return `ms-word:ofe|u|${presignedUrl}`
|
||||
}
|
||||
100
web/test/useVersionOpen.spec.ts
Normal file
100
web/test/useVersionOpen.spec.ts
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { createApp, type App } from 'vue'
|
||||
import { useVersionOpen } from '../app/composables/useVersionOpen'
|
||||
|
||||
let app: App
|
||||
let host: HTMLDivElement
|
||||
const apiJson = vi.fn()
|
||||
const replace = vi.fn()
|
||||
const close = vi.fn()
|
||||
let tab: { opener: unknown; closed: boolean; location: { replace: typeof replace }; close: typeof close }
|
||||
|
||||
function mount() {
|
||||
let result!: ReturnType<typeof useVersionOpen>
|
||||
host = document.createElement('div')
|
||||
document.body.append(host)
|
||||
app = createApp({ setup() { result = useVersionOpen(); return () => null } })
|
||||
app.mount(host)
|
||||
return result
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers()
|
||||
vi.stubGlobal('useApi', () => ({ apiJson }))
|
||||
tab = { opener: {}, closed: false, location: { replace }, close }
|
||||
vi.spyOn(window, 'open').mockReturnValue(tab as unknown as Window)
|
||||
})
|
||||
afterEach(() => {
|
||||
app?.unmount()
|
||||
host?.remove()
|
||||
vi.restoreAllMocks()
|
||||
vi.unstubAllGlobals()
|
||||
vi.clearAllMocks()
|
||||
vi.useRealTimers()
|
||||
})
|
||||
|
||||
describe('version open lifecycle', () => {
|
||||
it('reserves PDF tab synchronously, navigates after auth, leaves completed PDF open on unmount', async () => {
|
||||
let resolve!: (value: unknown) => void
|
||||
apiJson.mockReturnValue(new Promise(r => { resolve = r }))
|
||||
const state = mount()
|
||||
const pending = state.openVersion('d', 'v', 'pdf')
|
||||
expect(window.open).toHaveBeenCalledWith('about:blank', '_blank')
|
||||
expect(tab.opener).toBeNull()
|
||||
expect(state.openingVersionId.value).toBe('v')
|
||||
resolve({ url: 'https://files.example/v.pdf' })
|
||||
await pending
|
||||
expect(replace).toHaveBeenCalledWith('https://files.example/v.pdf')
|
||||
expect(state.fallbackUrl.value).toBe('https://files.example/v.pdf')
|
||||
app.unmount()
|
||||
expect(close).not.toHaveBeenCalled()
|
||||
})
|
||||
it('keeps a fallback when popup is blocked', async () => {
|
||||
vi.mocked(window.open).mockReturnValue(null)
|
||||
apiJson.mockResolvedValue({ url: 'https://files.example/v.pdf' })
|
||||
const state = mount()
|
||||
await state.openVersion('d', 'v', 'pdf')
|
||||
expect(state.openNotice.value).toBeTruthy()
|
||||
expect(state.fallbackUrl.value).toBe('https://files.example/v.pdf')
|
||||
})
|
||||
it('closes pending tab immediately on reset and ignores a late response', async () => {
|
||||
let resolve!: (value: unknown) => void
|
||||
apiJson.mockReturnValue(new Promise(r => { resolve = r }))
|
||||
const state = mount()
|
||||
const pending = state.openVersion('d', 'v', 'pdf')
|
||||
state.resetOpen()
|
||||
expect(close).toHaveBeenCalledTimes(1)
|
||||
resolve({ url: 'https://files.example/v.pdf' })
|
||||
await pending
|
||||
expect(replace).not.toHaveBeenCalled()
|
||||
expect(state.fallbackUrl.value).toBeNull()
|
||||
})
|
||||
it('closes reserved tab and reports request failure', async () => {
|
||||
apiJson.mockRejectedValue(new Error('offline'))
|
||||
const state = mount()
|
||||
await state.openVersion('d', 'v', 'pdf')
|
||||
expect(close).toHaveBeenCalled()
|
||||
expect(state.downloadError.value).toBe('offline')
|
||||
expect(state.openingVersionId.value).toBeNull()
|
||||
})
|
||||
it('handles synchronous popup errors and restores controls', async () => {
|
||||
vi.mocked(window.open).mockImplementation(() => { throw new Error('popup error') })
|
||||
const state = mount()
|
||||
await state.openVersion('d', 'v', 'pdf')
|
||||
expect(state.downloadError.value).toBe('popup error')
|
||||
expect(state.openingVersionId.value).toBeNull()
|
||||
})
|
||||
it('offers Word fallback after timeout and removes listeners on unmount', async () => {
|
||||
const assign = vi.spyOn(window.location, 'assign').mockImplementation(() => {})
|
||||
const remove = vi.spyOn(window, 'removeEventListener')
|
||||
apiJson.mockResolvedValue({ url: 'https://files.example/v.docx?signature=a%2Fb' })
|
||||
const state = mount()
|
||||
await state.openVersion('d', 'v')
|
||||
expect(assign).toHaveBeenCalledWith('ms-word:ofe|u|https://files.example/v.docx?signature=a%2Fb')
|
||||
vi.advanceTimersByTime(1500)
|
||||
expect(state.openNotice.value).toBeTruthy()
|
||||
expect(state.fallbackUrl.value).toBeTruthy()
|
||||
app.unmount()
|
||||
expect(remove).toHaveBeenCalledWith('blur', expect.any(Function))
|
||||
})
|
||||
})
|
||||
17
web/test/versionDownload.spec.ts
Normal file
17
web/test/versionDownload.spec.ts
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
import { describe, expect, it } from 'vitest'
|
||||
import { versionFileName, versionDownloadPath, wordOpenUrl } from '../app/utils/versionDownload'
|
||||
|
||||
describe('version downloads', () => {
|
||||
it('names v0 and later artifacts', () => {
|
||||
expect(versionFileName('คำร้อง.DOCX', 0)).toBe('คำร้อง_v0.docx')
|
||||
expect(versionFileName('คำร้อง.docx', 1, 'pdf')).toBe('คำร้อง_v1.pdf')
|
||||
})
|
||||
it('requests the authenticated download-link format', () => {
|
||||
expect(versionDownloadPath('d', 'v')).toBe('/api/documents/d/versions/v/download-link')
|
||||
expect(versionDownloadPath('d', 'v', 'pdf')).toBe('/api/documents/d/versions/v/download-link?format=pdf')
|
||||
})
|
||||
it('preserves the signed URL exactly inside the Office URI', () => {
|
||||
const url = 'https://files.example/d.docx?X-Amz-Signature=abc&response-content-disposition=attachment%3B%20filename%2A%3DUTF-8%27%27'
|
||||
expect(wordOpenUrl(url)).toBe(`ms-word:ofe|u|${url}`)
|
||||
})
|
||||
})
|
||||
Loading…
Add table
Add a link
Reference in a new issue