/** * MOCK — version API ฝั่ง client (รอ backend จริงตาม contract ใน docs/addin-ui-flow.md §7) * ต้นฉบับ: D:\word-addin-poc\addin\src\services\versioningService.ts (multipart POST ผ่าน apiClient) * * - คงชื่อ/ลายเซ็น uploadVersion/getVersions/getDownloadLink/newClientVersionId ไว้เป๊ะ — * วันปลด mock: ก้อบ body ของต้นฉบับกลับมาได้เลย * - ต้นฉบับมีอีก 2 ตัวที่ไม่มี caller ใน UI จึงไม่พอร์ต: buildDownloadUrl, downloadVersion * - **metadata ของ version ถูก persist ลง localStorage** (record เล็ก ๆ ต่อ documentId) — * ไม่งั้นปิดเอกสาร (taskpane ปิดตาม) แล้วเปิดใหม่ history ว่างเปล่า ทั้งที่ metadata * การ track อยู่ในไฟล์จริง ทำให้ทดสอบวง FR-18 ปิด→เปิดใหม่ไม่ได้; blob ของไฟล์จริง * ยังเก็บเฉพาะ session (mock download คืน about:blank อยู่แล้ว) — localStorage เข้า * ไม่ได้ (webview บางตัว block) = ใช้ memory อย่างเดียวแบบเดิม ไม่ throw */ import * as authService from "./authService"; import { generateGuid } from "./metadataService"; import { UploadVersionResult, VersionRecord } from "../models/versionRecord"; interface StoredVersion { record: VersionRecord; docxBlob: Blob | null; pdfBlob: Blob | null; } /** key = documentId, เก็บเรียงเก่า → ใหม่ */ const store = new Map(); const STORAGE_KEY = "wordaddin-mock-versions"; /** โหลด record ที่ persist ไว้ (เฉพาะ metadata — blob ไม่ข้าม session) — พังใด ๆ = เริ่มว่าง */ function loadPersisted(): Record { try { if (typeof window === "undefined") return {}; // SSR ของ client component — ไม่มี localStorage const raw = window.localStorage.getItem(STORAGE_KEY); const parsed = raw ? (JSON.parse(raw) as Record) : {}; return parsed && typeof parsed === "object" ? parsed : {}; } catch { return {}; } } const persisted = loadPersisted(); for (const [documentId, records] of Object.entries(persisted)) { if (Array.isArray(records)) { store.set(documentId, records.map((record) => ({ record, docxBlob: null, pdfBlob: null }))); } } /** บันทึกเฉพาะ metadata กลับลง localStorage — พัง = เงียบ (ยังมี memory ของ session นี้อยู่) */ function persist(): void { try { const snapshot: Record = {}; store.forEach((versions, documentId) => { snapshot[documentId] = versions.map((item) => item.record); }); window.localStorage.setItem(STORAGE_KEY, JSON.stringify(snapshot)); } catch { // ignore — quota/private mode } } function delay(ms: number): Promise { return new Promise((resolve) => setTimeout(resolve, ms)); } /** SHA-256 จริงของไฟล์ที่ได้จาก Word — พังก็ไม่ throw (ใช้ค่าหยง ๆ ให้ UI ยังไปต่อได้) */ async function mockContentHash(file: Blob): Promise { try { const digest = await crypto.subtle.digest("SHA-256", await file.arrayBuffer()); return Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join(""); } catch { return "mock-hash"; } } /** FR-5 — อัปโหลดไฟล์ปัจจุบันเป็น version ใหม่ (mock: เก็บลง memory) * + FR-13: message คือ commit message ทางเลือก * + FR-16: templateId เฉพาะไฟล์ร่าง (mock ไม่ใช้ — คงไว้ตาม signature ต้นฉบับ) */ export async function uploadVersion( documentId: string, file: Blob, clientVersionId: string, fields?: Record, fileName?: string, message?: string, pdf?: Blob, templateId?: string ): Promise { void clientVersionId; // mock ไม่ใช้ — id ของ mock สร้างเอง void fileName; void templateId; await delay(500); // ให้ข้อความ progress ของ flow ขึ้นจริง const versions = store.get(documentId) ?? []; const versionNumber = versions.reduce((max, item) => Math.max(max, item.record.versionNumber), 0) + 1; const hasPdf = !!pdf && pdf.size > 0; const trimmedMessage = message?.trim(); const record: VersionRecord = { versionId: generateGuid(), versionNumber, createdAt: new Date().toISOString(), createdByUserId: authService.getUsername() ?? "unknown", contentHash: await mockContentHash(file), message: trimmedMessage || null, hasPdf, fields: fields && Object.keys(fields).length > 0 ? fields : null, }; versions.push({ record, docxBlob: file, pdfBlob: pdf ?? null }); store.set(documentId, versions); persist(); return { hasPdf, versionId: record.versionId, versionNumber }; } /** FR-6 — version history เรียงใหม่ → เก่า (คืนสำเนากัน caller mutate store ตรง ๆ) */ export async function getVersions(documentId: string): Promise { await delay(200); const versions = store.get(documentId) ?? []; return versions .slice() .reverse() .map((item) => ({ ...item.record })); } export interface VersionDownloadLink { url: string; /** ISO-8601 เวลาที่ URL หมดอายุ */ expiresAt: string; } /** * FR-7 — ขอ presigned URL อายุสั้นของ version หนึ่ง เพื่อเปิดในเบราว์เซอร์ของผู้ใช้ * (webview ของ Word ทิ้ง blob download เงียบๆ — ให้เบราว์เซอร์จริงจัดการแทน) * mock: ไม่มี URL จริงให้คืน — blob:/data: ใช้ผ่าน openBrowserWindow ไม่ได้ (blob ผูกกับ * context ของ webview, data: โดนเบราว์เซอร์ block) ส่วน about:blank ที่คืนไปนี้ host จะ * ปฏิเสธเงียบๆ เพราะ openBrowserWindow เปิดเฉพาะ http/https และเป็น void API ไม่รายงานกลับ * — กดปุ่มดาวน์โหลด/PDF ตอน mock จึงไม่มีหน้าต่างเบราว์เซอร์เปิด (UI ยังเดินครบ flow) */ export async function getDownloadLink( documentId: string, versionId: string, format?: "docx" | "pdf" ): Promise { void documentId; void versionId; void format; await delay(150); return { url: "about:blank", expiresAt: new Date(Date.now() + 10 * 60_000).toISOString() }; } /** สร้าง clientVersionId ใหม่สำหรับ upload ครั้งถัดไป */ export function newClientVersionId(): string { return generateGuid(); }