2026-10-16 12:07:25 +07:00
|
|
|
/**
|
feat(addin): unmock versioning — real upload/download against docver API
Replace the in-memory versioningService with real calls to the docver
group (/api/documents/*) via apiClient with the ticket-issued Bearer;
keep the mock only for ?addindemo (authService.isDemoMode).
- uploadVersion: multipart POST /api/documents/versions (file, optional
pdf when non-empty, clientVersionId, fields JSON, trimmed message,
fileName) — identity fields are never sent (userId/userName/templateId
in the form = 400 per the 2026-10-22 contract; docId comes from the
fid claim)
- getVersions: GET → DocumentVersionDTO mapped to VersionRecord (adds
optional createdByUserName; history panel shows it with userId
fallback)
- getDownloadLink: GET presigned URL (+ ?format=pdf)
- apiClient.toUrl: empty NEXT_PUBLIC_ADDIN_API_BASE_URL = same-origin
through withBasePath (/docedit/api/...); the env is now only for a
split deployment (must include basePath, HTTPS)
- documentId/templateId params stay in signatures but are not sent
- tests: versioningService.test.ts covers both modes (real sign-in via
signInWithTicket, FormData contract assertions, DTO mapping, format
query, 401 -> AuthRequiredError, demo mock without network)
- docs sync: WORDADDIN-DEV (new "การรันกับ backend จริง" section),
addin-ui-flow status note, PLAN-DOCVER §8, PLAN-ADDIN-AUTH §12, CLAUDE.md
Verified: tsc clean, 286/286 tests, full E2E against MinIO (ticket ->
token -> upload docx / docx+pdf -> history -> presigned download of both
formats).
Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-27 12:52:29 +07:00
|
|
|
* Version API ฝั่ง client — **ของจริง** (กลุ่ม docver /api/documents/* ผ่าน apiClient
|
|
|
|
|
* ซึ่งแนบ Bearer แล้ว — contract ตาม docs/PLAN-DOCVER.md) · เหลือ mock เฉพาะโหมด
|
|
|
|
|
* `?addindemo` (dev-only ดู UI ใน browser ไม่ต้องมี backend/MinIO — authService.isDemoMode)
|
2026-10-16 12:07:25 +07:00
|
|
|
*
|
feat(addin): unmock versioning — real upload/download against docver API
Replace the in-memory versioningService with real calls to the docver
group (/api/documents/*) via apiClient with the ticket-issued Bearer;
keep the mock only for ?addindemo (authService.isDemoMode).
- uploadVersion: multipart POST /api/documents/versions (file, optional
pdf when non-empty, clientVersionId, fields JSON, trimmed message,
fileName) — identity fields are never sent (userId/userName/templateId
in the form = 400 per the 2026-10-22 contract; docId comes from the
fid claim)
- getVersions: GET → DocumentVersionDTO mapped to VersionRecord (adds
optional createdByUserName; history panel shows it with userId
fallback)
- getDownloadLink: GET presigned URL (+ ?format=pdf)
- apiClient.toUrl: empty NEXT_PUBLIC_ADDIN_API_BASE_URL = same-origin
through withBasePath (/docedit/api/...); the env is now only for a
split deployment (must include basePath, HTTPS)
- documentId/templateId params stay in signatures but are not sent
- tests: versioningService.test.ts covers both modes (real sign-in via
signInWithTicket, FormData contract assertions, DTO mapping, format
query, 401 -> AuthRequiredError, demo mock without network)
- docs sync: WORDADDIN-DEV (new "การรันกับ backend จริง" section),
addin-ui-flow status note, PLAN-DOCVER §8, PLAN-ADDIN-AUTH §12, CLAUDE.md
Verified: tsc clean, 286/286 tests, full E2E against MinIO (ticket ->
token -> upload docx / docx+pdf -> history -> presigned download of both
formats).
Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-27 12:52:29 +07:00
|
|
|
* - คงชื่อ/ลายเซ็น uploadVersion/getVersions/getDownloadLink/newClientVersionId ตามต้นฉบับ
|
|
|
|
|
* D:\word-addin-poc\addin\src\services\versioningService.ts — versionFlow เรียกต่อได้เดิม
|
|
|
|
|
* - **เอกสารถูกระบุด้วย fid ใน Bearer token** (ไม่ใช่ documentId GUID ที่ stamp ในไฟล์ —
|
|
|
|
|
* 2026-10-26 contract ถอด {docId} ออกจาก URL ทุก endpoint) พารามิเตอร์ documentId จึงเหลือ
|
|
|
|
|
* ไว้เพื่อ signature เดิมเท่านั้น ไม่ถูกส่งถึง server · templateId ก็เช่นกัน (มาจาก claims
|
|
|
|
|
* ของ token — ส่งมาใน form = 400)
|
|
|
|
|
* - **ห้ามส่ง userId/userName/templateId ใน FormData** (contract 2026-10-22 — ส่งมา = 400)
|
|
|
|
|
* - mock ยัง persist metadata ลง localStorage ต่อ documentId เพื่อทดสอบวงปิดเอกสาร→เปิดใหม่
|
|
|
|
|
* ในโหมด demo — blob ไฟล์จริงเฉพาะ session; localStorage เข้าไม่ได้ (webview บางตัว block)
|
|
|
|
|
* = ใช้ memory อย่างเดียว ไม่ throw
|
2026-10-16 12:07:25 +07:00
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
import * as authService from "./authService";
|
|
|
|
|
import { generateGuid } from "./metadataService";
|
fix(docver): drop fields value limit + treat history 404 as empty in add-in
Two fixes from first real-Word testing of the unmocked versioning flow:
- fields value length: remove the limit entirely (was 10,000, briefly
100,000) — real court documents run 100-200 pages with content
controls several hundred thousand chars long; callers are Bearer-only
and the column is Postgres jsonb (up to ~255MB), so no storage
bottleneck. Structural guards stay: ≤1,000 keys (was 100), key ≤200
chars, value must be string.
- getVersions in versioningService now maps the history 404 to an empty
list: files stamped SavedByAddIn back in the mock era enter the
tracking phase before any real upload exists, so the pane shows
"ยังไม่มี version ในระบบ" and Save & Version creates v1 (implicit
document creation) instead of surfacing a 404 error banner. Other
errors still propagate.
Tests: 288/288 (new cases — 1M-char value passes, key cap 1,000/1,001,
history 404 -> []). E2E against MinIO: 50k and 1M-char Thai field values
upload 200 and roundtrip byte-exact through jsonb. Docs synced:
PLAN-DOCVER §1, CLAUDE.md.
Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-27 13:14:04 +07:00
|
|
|
import { ApiError, apiGetJson, apiSendExpectJson } from "./apiClient";
|
2026-10-16 12:07:25 +07:00
|
|
|
import { UploadVersionResult, VersionRecord } from "../models/versionRecord";
|
|
|
|
|
|
feat(addin): unmock versioning — real upload/download against docver API
Replace the in-memory versioningService with real calls to the docver
group (/api/documents/*) via apiClient with the ticket-issued Bearer;
keep the mock only for ?addindemo (authService.isDemoMode).
- uploadVersion: multipart POST /api/documents/versions (file, optional
pdf when non-empty, clientVersionId, fields JSON, trimmed message,
fileName) — identity fields are never sent (userId/userName/templateId
in the form = 400 per the 2026-10-22 contract; docId comes from the
fid claim)
- getVersions: GET → DocumentVersionDTO mapped to VersionRecord (adds
optional createdByUserName; history panel shows it with userId
fallback)
- getDownloadLink: GET presigned URL (+ ?format=pdf)
- apiClient.toUrl: empty NEXT_PUBLIC_ADDIN_API_BASE_URL = same-origin
through withBasePath (/docedit/api/...); the env is now only for a
split deployment (must include basePath, HTTPS)
- documentId/templateId params stay in signatures but are not sent
- tests: versioningService.test.ts covers both modes (real sign-in via
signInWithTicket, FormData contract assertions, DTO mapping, format
query, 401 -> AuthRequiredError, demo mock without network)
- docs sync: WORDADDIN-DEV (new "การรันกับ backend จริง" section),
addin-ui-flow status note, PLAN-DOCVER §8, PLAN-ADDIN-AUTH §12, CLAUDE.md
Verified: tsc clean, 286/286 tests, full E2E against MinIO (ticket ->
token -> upload docx / docx+pdf -> history -> presigned download of both
formats).
Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-27 12:52:29 +07:00
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
// ของจริง — กลุ่ม docver (Bearer จาก ticket auth แนบอัตโนมัติโดย apiClient)
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
/** DTO ของ GET /api/documents/versions (ใหม่ → เก่า) — PLAN-DOCVER.md §1 */
|
|
|
|
|
interface DocumentVersionDTO {
|
|
|
|
|
versionId: string;
|
|
|
|
|
versionNumber: number;
|
|
|
|
|
createdAt: string;
|
|
|
|
|
createdByUserId: string;
|
|
|
|
|
createdByUserName: string;
|
|
|
|
|
contentHash: string;
|
|
|
|
|
fields: Record<string, string> | null;
|
|
|
|
|
message: string | null;
|
|
|
|
|
hasPdf: boolean;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** FR-5 — อัปโหลดไฟล์ปัจจุบันเป็น version ใหม่ (multipart POST)
|
|
|
|
|
* + FR-13: message คือ commit message ทางเลือก
|
|
|
|
|
* + FR-16: templateId เก็บไว้ตาม signature เดิม — ไม่ส่งถึง server (มาจาก claims ของ token) */
|
|
|
|
|
export async function uploadVersion(
|
|
|
|
|
documentId: string,
|
|
|
|
|
file: Blob,
|
|
|
|
|
clientVersionId: string,
|
|
|
|
|
fields?: Record<string, string>,
|
|
|
|
|
fileName?: string,
|
|
|
|
|
message?: string,
|
|
|
|
|
pdf?: Blob,
|
|
|
|
|
templateId?: string
|
|
|
|
|
): Promise<UploadVersionResult> {
|
|
|
|
|
if (!authService.isDemoMode()) {
|
|
|
|
|
void templateId; // contract 2026-10-22 — ห้ามส่ง (identity ทั้งหมดจาก Bearer token)
|
|
|
|
|
const form = new FormData();
|
|
|
|
|
form.append("file", file, fileName ?? "document.docx");
|
|
|
|
|
// pdf optional: ไม่แนบ/ไฟล์เปล่า = version นี้ไม่มีสำเนา PDF
|
|
|
|
|
if (pdf && pdf.size > 0) {
|
|
|
|
|
form.append("pdf", pdf, "document.pdf");
|
|
|
|
|
}
|
|
|
|
|
if (clientVersionId) {
|
|
|
|
|
form.append("clientVersionId", clientVersionId);
|
|
|
|
|
}
|
|
|
|
|
if (fields && Object.keys(fields).length > 0) {
|
|
|
|
|
form.append("fields", JSON.stringify(fields));
|
|
|
|
|
}
|
|
|
|
|
const trimmedMessage = message?.trim();
|
|
|
|
|
if (trimmedMessage) {
|
|
|
|
|
form.append("message", trimmedMessage);
|
|
|
|
|
}
|
|
|
|
|
if (fileName) {
|
|
|
|
|
form.append("fileName", fileName);
|
|
|
|
|
}
|
|
|
|
|
return apiSendExpectJson<UploadVersionResult>(
|
|
|
|
|
"/api/documents/versions",
|
|
|
|
|
{ method: "POST", body: form },
|
|
|
|
|
"อัปโหลด version ไม่สำเร็จ",
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
return uploadVersionMock(documentId, file, clientVersionId, fields, message, pdf);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** FR-6 — version history เรียงใหม่ → เก่า (คืนสำเนากัน caller mutate store ตรง ๆ)
|
fix(docver): drop fields value limit + treat history 404 as empty in add-in
Two fixes from first real-Word testing of the unmocked versioning flow:
- fields value length: remove the limit entirely (was 10,000, briefly
100,000) — real court documents run 100-200 pages with content
controls several hundred thousand chars long; callers are Bearer-only
and the column is Postgres jsonb (up to ~255MB), so no storage
bottleneck. Structural guards stay: ≤1,000 keys (was 100), key ≤200
chars, value must be string.
- getVersions in versioningService now maps the history 404 to an empty
list: files stamped SavedByAddIn back in the mock era enter the
tracking phase before any real upload exists, so the pane shows
"ยังไม่มี version ในระบบ" and Save & Version creates v1 (implicit
document creation) instead of surfacing a 404 error banner. Other
errors still propagate.
Tests: 288/288 (new cases — 1M-char value passes, key cap 1,000/1,001,
history 404 -> []). E2E against MinIO: 50k and 1M-char Thai field values
upload 200 and roundtrip byte-exact through jsonb. Docs synced:
PLAN-DOCVER §1, CLAUDE.md.
Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-27 13:14:04 +07:00
|
|
|
* docId มาจาก fid ใน token — documentId พารามิเตอร์ไม่ถูกใช้กับ server ของจริง
|
|
|
|
|
* 404 = เอกสารนี้ยังไม่เคยมี version (เช่น ไฟล์ที่ stamp SavedByAddIn ไว้สมัย mock แล้วเพิ่ง
|
|
|
|
|
* มาใช้ backend จริง) — สำหรับ add-in ความหมายเดียวกับ list ว่าง: UI แสดง "ยังไม่มี version
|
|
|
|
|
* ในระบบ" และกด Save & Version สร้าง v1 ได้เลย (upload สร้างเอกสารให้อัตโนมัติ) */
|
feat(addin): unmock versioning — real upload/download against docver API
Replace the in-memory versioningService with real calls to the docver
group (/api/documents/*) via apiClient with the ticket-issued Bearer;
keep the mock only for ?addindemo (authService.isDemoMode).
- uploadVersion: multipart POST /api/documents/versions (file, optional
pdf when non-empty, clientVersionId, fields JSON, trimmed message,
fileName) — identity fields are never sent (userId/userName/templateId
in the form = 400 per the 2026-10-22 contract; docId comes from the
fid claim)
- getVersions: GET → DocumentVersionDTO mapped to VersionRecord (adds
optional createdByUserName; history panel shows it with userId
fallback)
- getDownloadLink: GET presigned URL (+ ?format=pdf)
- apiClient.toUrl: empty NEXT_PUBLIC_ADDIN_API_BASE_URL = same-origin
through withBasePath (/docedit/api/...); the env is now only for a
split deployment (must include basePath, HTTPS)
- documentId/templateId params stay in signatures but are not sent
- tests: versioningService.test.ts covers both modes (real sign-in via
signInWithTicket, FormData contract assertions, DTO mapping, format
query, 401 -> AuthRequiredError, demo mock without network)
- docs sync: WORDADDIN-DEV (new "การรันกับ backend จริง" section),
addin-ui-flow status note, PLAN-DOCVER §8, PLAN-ADDIN-AUTH §12, CLAUDE.md
Verified: tsc clean, 286/286 tests, full E2E against MinIO (ticket ->
token -> upload docx / docx+pdf -> history -> presigned download of both
formats).
Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-27 12:52:29 +07:00
|
|
|
export async function getVersions(documentId: string): Promise<VersionRecord[]> {
|
|
|
|
|
if (!authService.isDemoMode()) {
|
fix(docver): drop fields value limit + treat history 404 as empty in add-in
Two fixes from first real-Word testing of the unmocked versioning flow:
- fields value length: remove the limit entirely (was 10,000, briefly
100,000) — real court documents run 100-200 pages with content
controls several hundred thousand chars long; callers are Bearer-only
and the column is Postgres jsonb (up to ~255MB), so no storage
bottleneck. Structural guards stay: ≤1,000 keys (was 100), key ≤200
chars, value must be string.
- getVersions in versioningService now maps the history 404 to an empty
list: files stamped SavedByAddIn back in the mock era enter the
tracking phase before any real upload exists, so the pane shows
"ยังไม่มี version ในระบบ" and Save & Version creates v1 (implicit
document creation) instead of surfacing a 404 error banner. Other
errors still propagate.
Tests: 288/288 (new cases — 1M-char value passes, key cap 1,000/1,001,
history 404 -> []). E2E against MinIO: 50k and 1M-char Thai field values
upload 200 and roundtrip byte-exact through jsonb. Docs synced:
PLAN-DOCVER §1, CLAUDE.md.
Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-27 13:14:04 +07:00
|
|
|
let rows: DocumentVersionDTO[];
|
|
|
|
|
try {
|
|
|
|
|
rows = await apiGetJson<DocumentVersionDTO[]>("/api/documents/versions", "โหลด version history ไม่สำเร็จ");
|
|
|
|
|
} catch (error) {
|
|
|
|
|
if (error instanceof ApiError && error.status === 404) {
|
|
|
|
|
return [];
|
|
|
|
|
}
|
|
|
|
|
throw error;
|
|
|
|
|
}
|
feat(addin): unmock versioning — real upload/download against docver API
Replace the in-memory versioningService with real calls to the docver
group (/api/documents/*) via apiClient with the ticket-issued Bearer;
keep the mock only for ?addindemo (authService.isDemoMode).
- uploadVersion: multipart POST /api/documents/versions (file, optional
pdf when non-empty, clientVersionId, fields JSON, trimmed message,
fileName) — identity fields are never sent (userId/userName/templateId
in the form = 400 per the 2026-10-22 contract; docId comes from the
fid claim)
- getVersions: GET → DocumentVersionDTO mapped to VersionRecord (adds
optional createdByUserName; history panel shows it with userId
fallback)
- getDownloadLink: GET presigned URL (+ ?format=pdf)
- apiClient.toUrl: empty NEXT_PUBLIC_ADDIN_API_BASE_URL = same-origin
through withBasePath (/docedit/api/...); the env is now only for a
split deployment (must include basePath, HTTPS)
- documentId/templateId params stay in signatures but are not sent
- tests: versioningService.test.ts covers both modes (real sign-in via
signInWithTicket, FormData contract assertions, DTO mapping, format
query, 401 -> AuthRequiredError, demo mock without network)
- docs sync: WORDADDIN-DEV (new "การรันกับ backend จริง" section),
addin-ui-flow status note, PLAN-DOCVER §8, PLAN-ADDIN-AUTH §12, CLAUDE.md
Verified: tsc clean, 286/286 tests, full E2E against MinIO (ticket ->
token -> upload docx / docx+pdf -> history -> presigned download of both
formats).
Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-27 12:52:29 +07:00
|
|
|
// server เรียงใหม่ → เก่าอยู่แล้ว — แปลงเป็น VersionRecord ตามที่ UI ใช้
|
|
|
|
|
return rows.map((row) => ({
|
|
|
|
|
versionId: row.versionId,
|
|
|
|
|
versionNumber: row.versionNumber,
|
|
|
|
|
createdAt: row.createdAt,
|
|
|
|
|
createdByUserId: row.createdByUserId,
|
|
|
|
|
createdByUserName: row.createdByUserName,
|
|
|
|
|
contentHash: row.contentHash,
|
|
|
|
|
fields: row.fields && Object.keys(row.fields).length > 0 ? row.fields : null,
|
|
|
|
|
message: row.message,
|
|
|
|
|
hasPdf: row.hasPdf,
|
|
|
|
|
}));
|
|
|
|
|
}
|
|
|
|
|
return getVersionsMock(documentId);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface VersionDownloadLink {
|
|
|
|
|
url: string;
|
|
|
|
|
/** ISO-8601 เวลาที่ URL หมดอายุ */
|
|
|
|
|
expiresAt: string;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* FR-7 — ขอ presigned URL อายุสั้นของ version หนึ่ง เพื่อเปิดในเบราว์เซอร์ของผู้ใช้
|
|
|
|
|
* (webview ของ Word ทิ้ง blob download เงียบๆ — ให้เบราว์เซอร์จริงจัดการแทน)
|
|
|
|
|
* URL เป็น MinIO presigned GET (อายุ env DOCVER_LINK_TTL_SECONDS default 900 วิ) —
|
|
|
|
|
* โฮสต์ MinIO ต้องเข้าถึงได้จากเครื่องผู้ใช้
|
|
|
|
|
*/
|
|
|
|
|
export async function getDownloadLink(
|
|
|
|
|
documentId: string,
|
|
|
|
|
versionId: string,
|
|
|
|
|
format?: "docx" | "pdf"
|
|
|
|
|
): Promise<VersionDownloadLink> {
|
|
|
|
|
if (!authService.isDemoMode()) {
|
|
|
|
|
void documentId; // เอกสารมาจาก fid ใน token — versionId เช็คคู่กับ docId ฝั่ง server
|
|
|
|
|
const query = format && format !== "docx" ? `?format=${encodeURIComponent(format)}` : "";
|
|
|
|
|
return apiGetJson<VersionDownloadLink>(
|
|
|
|
|
`/api/documents/versions/${encodeURIComponent(versionId)}/download-link${query}`,
|
|
|
|
|
"ขอลิงก์ดาวน์โหลดไม่สำเร็จ",
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
return getDownloadLinkMock();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** สร้าง clientVersionId ใหม่สำหรับ upload ครั้งถัดไป */
|
|
|
|
|
export function newClientVersionId(): string {
|
|
|
|
|
return generateGuid();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
// Mock — เฉพาะโหมด ?addindemo (dev-only)
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
2026-10-16 12:07:25 +07:00
|
|
|
interface StoredVersion {
|
|
|
|
|
record: VersionRecord;
|
|
|
|
|
docxBlob: Blob | null;
|
|
|
|
|
pdfBlob: Blob | null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** key = documentId, เก็บเรียงเก่า → ใหม่ */
|
|
|
|
|
const store = new Map<string, StoredVersion[]>();
|
|
|
|
|
|
|
|
|
|
const STORAGE_KEY = "wordaddin-mock-versions";
|
|
|
|
|
|
|
|
|
|
/** โหลด record ที่ persist ไว้ (เฉพาะ metadata — blob ไม่ข้าม session) — พังใด ๆ = เริ่มว่าง */
|
|
|
|
|
function loadPersisted(): Record<string, VersionRecord[]> {
|
|
|
|
|
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<string, VersionRecord[]>) : {};
|
|
|
|
|
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<string, VersionRecord[]> = {};
|
|
|
|
|
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<void> {
|
|
|
|
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** SHA-256 จริงของไฟล์ที่ได้จาก Word — พังก็ไม่ throw (ใช้ค่าหยง ๆ ให้ UI ยังไปต่อได้) */
|
|
|
|
|
async function mockContentHash(file: Blob): Promise<string> {
|
|
|
|
|
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";
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
feat(addin): unmock versioning — real upload/download against docver API
Replace the in-memory versioningService with real calls to the docver
group (/api/documents/*) via apiClient with the ticket-issued Bearer;
keep the mock only for ?addindemo (authService.isDemoMode).
- uploadVersion: multipart POST /api/documents/versions (file, optional
pdf when non-empty, clientVersionId, fields JSON, trimmed message,
fileName) — identity fields are never sent (userId/userName/templateId
in the form = 400 per the 2026-10-22 contract; docId comes from the
fid claim)
- getVersions: GET → DocumentVersionDTO mapped to VersionRecord (adds
optional createdByUserName; history panel shows it with userId
fallback)
- getDownloadLink: GET presigned URL (+ ?format=pdf)
- apiClient.toUrl: empty NEXT_PUBLIC_ADDIN_API_BASE_URL = same-origin
through withBasePath (/docedit/api/...); the env is now only for a
split deployment (must include basePath, HTTPS)
- documentId/templateId params stay in signatures but are not sent
- tests: versioningService.test.ts covers both modes (real sign-in via
signInWithTicket, FormData contract assertions, DTO mapping, format
query, 401 -> AuthRequiredError, demo mock without network)
- docs sync: WORDADDIN-DEV (new "การรันกับ backend จริง" section),
addin-ui-flow status note, PLAN-DOCVER §8, PLAN-ADDIN-AUTH §12, CLAUDE.md
Verified: tsc clean, 286/286 tests, full E2E against MinIO (ticket ->
token -> upload docx / docx+pdf -> history -> presigned download of both
formats).
Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-27 12:52:29 +07:00
|
|
|
async function uploadVersionMock(
|
2026-10-16 12:07:25 +07:00
|
|
|
documentId: string,
|
|
|
|
|
file: Blob,
|
|
|
|
|
clientVersionId: string,
|
|
|
|
|
fields?: Record<string, string>,
|
|
|
|
|
message?: string,
|
feat(addin): unmock versioning — real upload/download against docver API
Replace the in-memory versioningService with real calls to the docver
group (/api/documents/*) via apiClient with the ticket-issued Bearer;
keep the mock only for ?addindemo (authService.isDemoMode).
- uploadVersion: multipart POST /api/documents/versions (file, optional
pdf when non-empty, clientVersionId, fields JSON, trimmed message,
fileName) — identity fields are never sent (userId/userName/templateId
in the form = 400 per the 2026-10-22 contract; docId comes from the
fid claim)
- getVersions: GET → DocumentVersionDTO mapped to VersionRecord (adds
optional createdByUserName; history panel shows it with userId
fallback)
- getDownloadLink: GET presigned URL (+ ?format=pdf)
- apiClient.toUrl: empty NEXT_PUBLIC_ADDIN_API_BASE_URL = same-origin
through withBasePath (/docedit/api/...); the env is now only for a
split deployment (must include basePath, HTTPS)
- documentId/templateId params stay in signatures but are not sent
- tests: versioningService.test.ts covers both modes (real sign-in via
signInWithTicket, FormData contract assertions, DTO mapping, format
query, 401 -> AuthRequiredError, demo mock without network)
- docs sync: WORDADDIN-DEV (new "การรันกับ backend จริง" section),
addin-ui-flow status note, PLAN-DOCVER §8, PLAN-ADDIN-AUTH §12, CLAUDE.md
Verified: tsc clean, 286/286 tests, full E2E against MinIO (ticket ->
token -> upload docx / docx+pdf -> history -> presigned download of both
formats).
Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-27 12:52:29 +07:00
|
|
|
pdf?: Blob
|
2026-10-16 12:07:25 +07:00
|
|
|
): Promise<UploadVersionResult> {
|
|
|
|
|
void clientVersionId; // mock ไม่ใช้ — id ของ mock สร้างเอง
|
|
|
|
|
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 };
|
|
|
|
|
}
|
|
|
|
|
|
feat(addin): unmock versioning — real upload/download against docver API
Replace the in-memory versioningService with real calls to the docver
group (/api/documents/*) via apiClient with the ticket-issued Bearer;
keep the mock only for ?addindemo (authService.isDemoMode).
- uploadVersion: multipart POST /api/documents/versions (file, optional
pdf when non-empty, clientVersionId, fields JSON, trimmed message,
fileName) — identity fields are never sent (userId/userName/templateId
in the form = 400 per the 2026-10-22 contract; docId comes from the
fid claim)
- getVersions: GET → DocumentVersionDTO mapped to VersionRecord (adds
optional createdByUserName; history panel shows it with userId
fallback)
- getDownloadLink: GET presigned URL (+ ?format=pdf)
- apiClient.toUrl: empty NEXT_PUBLIC_ADDIN_API_BASE_URL = same-origin
through withBasePath (/docedit/api/...); the env is now only for a
split deployment (must include basePath, HTTPS)
- documentId/templateId params stay in signatures but are not sent
- tests: versioningService.test.ts covers both modes (real sign-in via
signInWithTicket, FormData contract assertions, DTO mapping, format
query, 401 -> AuthRequiredError, demo mock without network)
- docs sync: WORDADDIN-DEV (new "การรันกับ backend จริง" section),
addin-ui-flow status note, PLAN-DOCVER §8, PLAN-ADDIN-AUTH §12, CLAUDE.md
Verified: tsc clean, 286/286 tests, full E2E against MinIO (ticket ->
token -> upload docx / docx+pdf -> history -> presigned download of both
formats).
Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-27 12:52:29 +07:00
|
|
|
async function getVersionsMock(documentId: string): Promise<VersionRecord[]> {
|
2026-10-16 12:07:25 +07:00
|
|
|
await delay(200);
|
|
|
|
|
const versions = store.get(documentId) ?? [];
|
|
|
|
|
return versions
|
|
|
|
|
.slice()
|
|
|
|
|
.reverse()
|
|
|
|
|
.map((item) => ({ ...item.record }));
|
|
|
|
|
}
|
|
|
|
|
|
feat(addin): unmock versioning — real upload/download against docver API
Replace the in-memory versioningService with real calls to the docver
group (/api/documents/*) via apiClient with the ticket-issued Bearer;
keep the mock only for ?addindemo (authService.isDemoMode).
- uploadVersion: multipart POST /api/documents/versions (file, optional
pdf when non-empty, clientVersionId, fields JSON, trimmed message,
fileName) — identity fields are never sent (userId/userName/templateId
in the form = 400 per the 2026-10-22 contract; docId comes from the
fid claim)
- getVersions: GET → DocumentVersionDTO mapped to VersionRecord (adds
optional createdByUserName; history panel shows it with userId
fallback)
- getDownloadLink: GET presigned URL (+ ?format=pdf)
- apiClient.toUrl: empty NEXT_PUBLIC_ADDIN_API_BASE_URL = same-origin
through withBasePath (/docedit/api/...); the env is now only for a
split deployment (must include basePath, HTTPS)
- documentId/templateId params stay in signatures but are not sent
- tests: versioningService.test.ts covers both modes (real sign-in via
signInWithTicket, FormData contract assertions, DTO mapping, format
query, 401 -> AuthRequiredError, demo mock without network)
- docs sync: WORDADDIN-DEV (new "การรันกับ backend จริง" section),
addin-ui-flow status note, PLAN-DOCVER §8, PLAN-ADDIN-AUTH §12, CLAUDE.md
Verified: tsc clean, 286/286 tests, full E2E against MinIO (ticket ->
token -> upload docx / docx+pdf -> history -> presigned download of both
formats).
Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-27 12:52:29 +07:00
|
|
|
async function getDownloadLinkMock(): Promise<VersionDownloadLink> {
|
2026-10-16 12:07:25 +07:00
|
|
|
await delay(150);
|
|
|
|
|
return { url: "about:blank", expiresAt: new Date(Date.now() + 10 * 60_000).toISOString() };
|
|
|
|
|
}
|