132 lines
5.8 KiB
TypeScript
132 lines
5.8 KiB
TypeScript
|
|
"use client";
|
||
|
|
|
||
|
|
import { useEffect, useState } from "react";
|
||
|
|
|
||
|
|
import { withBasePath } from "@/utils/base-path";
|
||
|
|
import { signInForDemo } from "@/wordaddin/services/authService";
|
||
|
|
|
||
|
|
import { MessageBar } from "./MessageBar";
|
||
|
|
import { Spinner } from "./Spinner";
|
||
|
|
import TaskpaneApp from "./TaskpaneApp";
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Gate ของ taskpane ใน App Router — ทำหน้าที่แทน Office.onReady ก่อน render ของ webpack
|
||
|
|
* ต้นฉบับ: Office.js โหลดใน <head> ของ taskpane.html แล้ว Office.onReady ก่อน createRoot
|
||
|
|
* (บริบท Next: ฉีด <script> self-host หลัง hydration แทน — ไฟล์ services ทั้งหมดไม่สัมผัส
|
||
|
|
* Office ที่ module top-level จึงปลอดภัย)
|
||
|
|
*
|
||
|
|
* โหมด ?addindemo=untracked|tracking (เฉพาะ dev) — ข้าม office.js ทั้งหมด + sign in mock
|
||
|
|
* ทันที เพื่อดู UI ทุก phase ใน browser ธรรมดา (mock versioningService ทำงานได้เอง
|
||
|
|
* แต่ flow ที่แตะ Word จริงจะจบที่ error ตาม expected — ดู docs/WORDADDIN-DEV.md)
|
||
|
|
*/
|
||
|
|
|
||
|
|
/** office.js self-host จาก postinstall scripts/fetch-office-js.mjs (intranet ไม่ใช้ CDN) */
|
||
|
|
const OFFICE_JS_SRC = withBasePath("/wordaddin/office-js/office.js");
|
||
|
|
/** Office.onReady callback ไม่ยิงภายใน 5 วิหลังฉีด script ก็ proceed (กัน webview แปลก ๆ — โหลดสำเร็จแต่ onReady เงียบ) */
|
||
|
|
const ONREADY_FALLBACK_MS = 5_000;
|
||
|
|
|
||
|
|
/**
|
||
|
|
* โหลด office.js ครั้งเดียวต่อ page เป็น singleton promise ระดับ module —
|
||
|
|
* ห้ามฉีด <script> ซ้ำ: React StrictMode รัน effect สองรอบใน dev และ guard
|
||
|
|
* `typeof Office !== "undefined"` ไม่ครอบช่วงที่ตัวแรกยังโหลดไม่เสร็จ
|
||
|
|
* (โหลดซ้ำ = evaluate ซ้ำ = "Cannot redefine property: context" เพราะ office.js
|
||
|
|
* define `Microsoft.Office.WebExtension.context` แบบ configurable:false)
|
||
|
|
* error → คืน promise ใหม่ครั้งหน้า (ลบ script เก่าทิ้งให้ inject ใหม่ได้)
|
||
|
|
*/
|
||
|
|
let officeReadyPromise: Promise<boolean> | null = null;
|
||
|
|
|
||
|
|
function ensureOfficeJs(): Promise<boolean> {
|
||
|
|
if (!officeReadyPromise) {
|
||
|
|
officeReadyPromise = new Promise<boolean>((resolve) => {
|
||
|
|
if (typeof Office !== "undefined") {
|
||
|
|
void Office.onReady(() => resolve(true));
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
const script = document.createElement("script");
|
||
|
|
const fallbackTimer = setTimeout(() => resolve(true), ONREADY_FALLBACK_MS);
|
||
|
|
const onReady = () => {
|
||
|
|
// เคลียร์ timer เฉพาะเมื่อ callback ของ Office.onReady ยิงจริง — ถ้าเคลียร์ตั้งแต่ onload
|
||
|
|
// แล้ว onReady เงียบ fallback จะตายไปก่อนทำหน้าที่ (ค้าง loading ตลอดไป)
|
||
|
|
void Office.onReady(() => {
|
||
|
|
clearTimeout(fallbackTimer);
|
||
|
|
resolve(true);
|
||
|
|
});
|
||
|
|
};
|
||
|
|
script.onload = onReady;
|
||
|
|
script.onerror = () => {
|
||
|
|
clearTimeout(fallbackTimer);
|
||
|
|
officeReadyPromise = null;
|
||
|
|
script.remove();
|
||
|
|
resolve(false);
|
||
|
|
};
|
||
|
|
script.src = OFFICE_JS_SRC;
|
||
|
|
script.async = false; // ให้ script นี้ได้ทำงานตามลำดับก่อนโค้ดที่แตะ Office
|
||
|
|
script.id = "office-js";
|
||
|
|
document.head.appendChild(script);
|
||
|
|
});
|
||
|
|
}
|
||
|
|
return officeReadyPromise;
|
||
|
|
}
|
||
|
|
|
||
|
|
type GateStatus = "loading" | "ready" | "error";
|
||
|
|
|
||
|
|
export function WordAddinGate() {
|
||
|
|
const [status, setStatus] = useState<GateStatus>("loading");
|
||
|
|
const [demo, setDemo] = useState<"untracked" | "tracking" | null>(null);
|
||
|
|
|
||
|
|
useEffect(() => {
|
||
|
|
const demoParam = new URLSearchParams(window.location.search).get("addindemo");
|
||
|
|
if (process.env.NODE_ENV !== "production" && (demoParam === "untracked" || demoParam === "tracking")) {
|
||
|
|
signInForDemo();
|
||
|
|
setDemo(demoParam);
|
||
|
|
setStatus("ready");
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
let cancelled = false;
|
||
|
|
void ensureOfficeJs()
|
||
|
|
.then((ok) => {
|
||
|
|
if (!cancelled) setStatus(ok ? "ready" : "error");
|
||
|
|
})
|
||
|
|
.catch(() => {
|
||
|
|
// Office.onReady throw sync ใน executor กลายเป็น rejection — ไป error screen แทนค้าง loading
|
||
|
|
if (!cancelled) setStatus("error");
|
||
|
|
});
|
||
|
|
return () => {
|
||
|
|
cancelled = true;
|
||
|
|
};
|
||
|
|
}, []);
|
||
|
|
|
||
|
|
if (status === "loading") {
|
||
|
|
return (
|
||
|
|
<div className="flex min-h-screen items-center justify-center">
|
||
|
|
<Spinner label="กำลังเริ่มต้น…" className="text-ink-soft" />
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
}
|
||
|
|
if (status === "error") {
|
||
|
|
return (
|
||
|
|
<div className="flex min-h-screen items-center justify-center p-4">
|
||
|
|
<MessageBar
|
||
|
|
intent="error"
|
||
|
|
action={
|
||
|
|
<button
|
||
|
|
type="button"
|
||
|
|
className="focus-ring rounded-md border border-error/40 bg-white px-2 py-1.5 text-xs text-error-ink"
|
||
|
|
onClick={() => window.location.reload()}
|
||
|
|
>
|
||
|
|
ลองใหม่
|
||
|
|
</button>
|
||
|
|
}
|
||
|
|
>
|
||
|
|
โหลด Office.js ไม่สำเร็จ — เช็คว่า dev server รันอยู่และไฟล์ public/wordaddin/office-js/office.js
|
||
|
|
ถูกสร้างแล้ว (npm install รัน postinstall scripts/fetch-office-js.mjs)
|
||
|
|
</MessageBar>
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
}
|
||
|
|
return <TaskpaneApp demo={demo ?? undefined} />;
|
||
|
|
}
|
||
|
|
|
||
|
|
export default WordAddinGate;
|