Merge branch 'forgejo-dev' into newfeat
Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
commit
c57a75e36a
50 changed files with 2760 additions and 961 deletions
114
components/TmplHome.tsx
Normal file
114
components/TmplHome.tsx
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
// components/TemplateFormClient.tsx
|
||||
'use client';
|
||||
import { useState, type ChangeEvent } from 'react';
|
||||
import Link from "next/link";
|
||||
import DependentDropdown, { type DropdownData } from '@/components/utils/DependentDropdown';
|
||||
|
||||
interface TemplateFormClientProps {
|
||||
templateData: DropdownData;
|
||||
}
|
||||
|
||||
export default function TemplateFormClient({ templateData }: TemplateFormClientProps) {
|
||||
const handleTmplSelect = (template: string, data: string) => {
|
||||
// Handle selection if needed
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
|
||||
|
||||
|
||||
<div className="mt-5 grid gap-5 sm:grid-cols-2">
|
||||
{/* ส่วน Template & Data แสดงรายการไฟล์จริงจากโฟลเดอร์ */}
|
||||
<div className="flex gap-4 rounded-xl border border-line bg-white p-6">
|
||||
<div>
|
||||
<h3 className="font-display text-subhead text-ink">Template & Data</h3>
|
||||
<div className="mt-1.5 text-sm leading-relaxed text-ink-soft">
|
||||
{Object.entries(templateData).map(([tid, jsonList]) => (
|
||||
<div key={tid} className="mb-3">
|
||||
<b>Template: </b>
|
||||
<Link href={`/tmpl/${tid}.docx`} className="text-primary transition hover:underline">
|
||||
{tid}.docx
|
||||
</Link>
|
||||
<br />
|
||||
<b>Data: </b>
|
||||
{jsonList.map((fid, idx) => (
|
||||
<span key={fid}>
|
||||
<Link href={`/tmpl/${fid}.json`} className="text-primary transition hover:underline">
|
||||
{fid}.json
|
||||
</Link>
|
||||
{idx < jsonList.length - 1 ? ', ' : ''}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
<div className="mt-2">
|
||||
นำไฟล์ไปทดสอบกับ <Link href="/api/swagger/" target="_top" className="text-primary transition hover:underline">API</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/* ส่วน Form (POST) */}
|
||||
<div className="flex gap-4 rounded-xl border border-line bg-white p-6">
|
||||
<div>
|
||||
<h3 className="font-display text-subhead text-ink">Form (POST)</h3>
|
||||
<div className="mt-1.5 text-sm leading-relaxed text-ink-soft">
|
||||
<form action="/docedit/api/template/download-docx" method="POST">
|
||||
<DependentDropdown data={templateData} name1='tid' name2='fid'
|
||||
label1="Template(tid):" label2="ข้อมูล(fid):" onSelectChange={handleTmplSelect} />
|
||||
<div className="mb-2"></div>
|
||||
<div>
|
||||
<label htmlFor="uid">uid:
|
||||
<input type="text" name="uid" className="p-2 mb-2 border border-gray-300 rounded-md" defaultValue="john" />
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="uname">uname:
|
||||
<input type="text" name="uname" className="p-2 mb-2 border border-gray-300 rounded-md" defaultValue="John Doe" />
|
||||
</label>
|
||||
</div>
|
||||
<button
|
||||
type="submit"
|
||||
style={{
|
||||
padding: '10px 20px',
|
||||
backgroundColor: '#0070f3',
|
||||
color: '#fff',
|
||||
border: 'none',
|
||||
borderRadius: '5px',
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
>
|
||||
ดาวน์โหลดเอกสาร MS Word
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/* ส่วน Link (GET) */}
|
||||
<div className="flex gap-4 rounded-xl border border-line bg-white p-6">
|
||||
<div>
|
||||
<h3 className="font-display text-subhead text-ink">Link (GET)</h3>
|
||||
<p className="mt-1.5 text-sm leading-relaxed text-ink-soft">
|
||||
{Object.entries(templateData).map(([tid, jsonList]) => {
|
||||
// jsonList คืออาเรย์ของชื่อไฟล์ json (fid) ที่ผูกอยู่กับ template (tid) นี้
|
||||
return jsonList.map((fid) => {
|
||||
const linkHref = `/docedit/api/template/download-docx?tid=${tid}&fid=${fid}&uid=john&uname=John%20Doe`;
|
||||
const displayLabel = `${fid}_john_xxx.docx`; // หรือปรับรูปแบบข้อความแสดงผลตามต้องการ
|
||||
|
||||
return (
|
||||
<span key={`${tid}-${fid}`}>
|
||||
<a href={linkHref} className="text-primary transition hover:underline">
|
||||
{displayLabel}
|
||||
</a>
|
||||
<br />
|
||||
</span>
|
||||
);
|
||||
});
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -114,6 +114,10 @@ const TaskpaneApp: React.FC<{ demo?: "untracked" | "tracking" }> = ({ demo }) =>
|
|||
|
||||
/** ข้อความบนหน้า no-access — ต่างกันตามสาเหตุ (ไม่มี token ในไฟล์ / token หมดอายุ) */
|
||||
const [noAccessNotice, setNoAccessNotice] = useState<string | null>(null);
|
||||
/** no-access ที่ "ลองใหม่" ช่วยได้ — เฉพาะเคสอ่านไฟล์ไม่สำเร็จ (เช่น office.js มาช้า
|
||||
* บนเครื่องช้า หรือ Word สะดุดชั่วคราว) กดแล้ววน detect() ใหม่ · เคสอื่น (ticket
|
||||
* หมดอายุ/ลายเซ็นไม่ผ่าน/ขอ token ไม่ได้) retry ไม่ช่วย ต้องขอไฟล์ใหม่/เน็ตกลับมา */
|
||||
const [noAccessRetryable, setNoAccessRetryable] = useState(false);
|
||||
|
||||
const [starting, setStarting] = useState(false);
|
||||
const [saving, setSaving] = useState(false);
|
||||
|
|
@ -183,6 +187,7 @@ const TaskpaneApp: React.FC<{ demo?: "untracked" | "tracking" }> = ({ demo }) =>
|
|||
const detect = useCallback(async () => {
|
||||
setPhase("checking");
|
||||
setBootError(null);
|
||||
setNoAccessRetryable(false);
|
||||
try {
|
||||
let properties: metadataService.TrackedDocumentProperties | null = null;
|
||||
try {
|
||||
|
|
@ -191,7 +196,8 @@ const TaskpaneApp: React.FC<{ demo?: "untracked" | "tracking" }> = ({ demo }) =>
|
|||
properties = null;
|
||||
}
|
||||
if (!properties) {
|
||||
setNoAccessNotice("อ่านข้อมูลเอกสารไม่สำเร็จ — ลองปิดแล้วเปิด task pane อีกครั้ง");
|
||||
setNoAccessNotice("อ่านข้อมูลเอกสารไม่สำเร็จ — ลองกด \"ลองใหม่\" หรือปิดแล้วเปิด task pane อีกครั้ง");
|
||||
setNoAccessRetryable(true);
|
||||
setPhase("no-access");
|
||||
return;
|
||||
}
|
||||
|
|
@ -206,6 +212,10 @@ const TaskpaneApp: React.FC<{ demo?: "untracked" | "tracking" }> = ({ demo }) =>
|
|||
}
|
||||
setUsername(authService.getUsername() ?? null);
|
||||
|
||||
// auto-open (2026-11-05) — sign-in ผ่าน = ไฟล์ e-Court: tag ให้ taskpane เด้งเองครั้งถัดไป
|
||||
// ที่เปิดไฟล์นี้ · fire-and-forget (service กลืน error ทั้งหมด — ไฟล์ทั่วไปไม่มาทางนี้อยู่แล้ว)
|
||||
void metadataService.enableAutoOpenTaskpane();
|
||||
|
||||
const tracked = properties.savedByAddIn === metadataService.ADDIN_SIGNATURE_GUID;
|
||||
const id = tracked ? properties.documentTrackingId : undefined;
|
||||
if (!id) {
|
||||
|
|
@ -469,6 +479,15 @@ const TaskpaneApp: React.FC<{ demo?: "untracked" | "tracking" }> = ({ demo }) =>
|
|||
<p className="text-sm text-ink-soft">
|
||||
{noAccessNotice ?? "ไม่พบ ticket ของ e-Court ในไฟล์ — ขณะนี้ add-in ใช้ได้กับเอกสารที่ส่งมาจาก e-Court เท่านั้น"}
|
||||
</p>
|
||||
{noAccessRetryable ? (
|
||||
<button
|
||||
type="button"
|
||||
className="focus-ring w-fit rounded-md border border-error/40 bg-white px-2 py-1.5 text-xs text-error-ink"
|
||||
onClick={() => void detect()}
|
||||
>
|
||||
ลองใหม่
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
|
|
|
|||
|
|
@ -22,8 +22,41 @@ import TaskpaneApp from "./TaskpaneApp";
|
|||
|
||||
/** 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 เงียบ) */
|
||||
/**
|
||||
* ช่วง poll ของ fallback — ทุก 5 วิเช็คว่า global `Office` โผล่แล้วหรือยัง:
|
||||
* โผล่แล้วแต่ onReady เงียบ (webview แปลก ๆ) = proceed ตามเจตนาเดิม · ยังไม่โผล่
|
||||
* (เครื่อง/เน็ตช้า — script ยังโหลดไม่เสร็จ) = รอต่อรอบหน้า ไม่ proceed บอด ๆ
|
||||
*/
|
||||
const ONREADY_FALLBACK_MS = 5_000;
|
||||
/** เพดานรวมหลัง inject script — ครบแล้วยังไม่มี Office = ไปหน้า error (มีปุ่มลองใหม่) */
|
||||
const ONREADY_HARD_CAP_MS = 60_000;
|
||||
|
||||
/**
|
||||
* office.js **ลบ `history.pushState`/`replaceState` ทิ้ง** ใน taskpane (set เป็น null —
|
||||
* กัน add-in พา host ไปที่อื่น: office-js#429) แต่ App Router ของ Next ต้องใช้ History API
|
||||
* ตอน mount ไม่งั้นพัง "window.history.replaceState is not a function" — เก็บตัวจริงไว้
|
||||
* ก่อนฉีด script แล้วคืนคืนหลัง office.js โหลด/พร้อม (workaround มาตรฐานของ SPA ใน
|
||||
* add-in — taskpane เป็น iframe ของตัวเอง Next ใช้ history เฉพาะ SPA state ภายใน)
|
||||
*/
|
||||
const nativeHistory: { pushState?: History["pushState"]; replaceState?: History["replaceState"] } =
|
||||
typeof window === "undefined"
|
||||
? {}
|
||||
: {
|
||||
pushState: typeof window.history.pushState === "function" ? window.history.pushState.bind(window.history) : undefined,
|
||||
replaceState:
|
||||
typeof window.history.replaceState === "function" ? window.history.replaceState.bind(window.history) : undefined,
|
||||
};
|
||||
|
||||
/** คืน History API ที่ office.js ลบทิ้ง — idempotent (มีอยู่แล้ว = ไม่แตะ) */
|
||||
function restoreHistoryApi(): void {
|
||||
if (typeof window === "undefined") return;
|
||||
if (typeof window.history.pushState !== "function" && nativeHistory.pushState) {
|
||||
window.history.pushState = nativeHistory.pushState;
|
||||
}
|
||||
if (typeof window.history.replaceState !== "function" && nativeHistory.replaceState) {
|
||||
window.history.replaceState = nativeHistory.replaceState;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* โหลด office.js ครั้งเดียวต่อ page เป็น singleton promise ระดับ module —
|
||||
|
|
@ -39,16 +72,44 @@ function ensureOfficeJs(): Promise<boolean> {
|
|||
if (!officeReadyPromise) {
|
||||
officeReadyPromise = new Promise<boolean>((resolve) => {
|
||||
if (typeof Office !== "undefined") {
|
||||
void Office.onReady(() => resolve(true));
|
||||
restoreHistoryApi(); // office.js evaluate ไปแล้ว — คืนก่อนให้ router ใช้ต่อได้
|
||||
void Office.onReady(() => {
|
||||
restoreHistoryApi(); // host handshake อาจลบซ้ำตอน initialize
|
||||
resolve(true);
|
||||
});
|
||||
return;
|
||||
}
|
||||
const script = document.createElement("script");
|
||||
const fallbackTimer = setTimeout(() => resolve(true), ONREADY_FALLBACK_MS);
|
||||
const startedAt = Date.now();
|
||||
let fallbackTimer: ReturnType<typeof setTimeout>;
|
||||
const armFallback = () => {
|
||||
fallbackTimer = setTimeout(() => {
|
||||
// Office โผล่แล้ว (script evaluate แล้ว) = proceed ได้เลย ไม่รอ onReady —
|
||||
// กัน webview ที่โหลดสำเร็จแต่ callback เงียบ (เจตนาเดิมของ fallback)
|
||||
if (typeof Office !== "undefined") {
|
||||
restoreHistoryApi(); // office.js evaluate แล้วอาจลบ history ทิ้ง — คืนก่อน proceed
|
||||
resolve(true);
|
||||
return;
|
||||
}
|
||||
// ยังไม่มี Office global = เครื่องช้า script ยังไม่โหลดเสร็จ — รอต่อ (re-arm)
|
||||
// จนครบเพดานค่อยยอมแพ้ไปหน้า error กัน proceed แล้ว Word.run พังกลางคัน
|
||||
if (Date.now() - startedAt >= ONREADY_HARD_CAP_MS) {
|
||||
officeReadyPromise = null;
|
||||
script.remove();
|
||||
resolve(false);
|
||||
return;
|
||||
}
|
||||
armFallback();
|
||||
}, ONREADY_FALLBACK_MS);
|
||||
};
|
||||
armFallback(); // ตั้งก่อนเสียบ src เหมือนเดิม — ครอบทั้ง network fetch + evaluate + รอ onReady
|
||||
const onReady = () => {
|
||||
// เคลียร์ timer เฉพาะเมื่อ callback ของ Office.onReady ยิงจริง — ถ้าเคลียร์ตั้งแต่ onload
|
||||
// แล้ว onReady เงียบ fallback จะตายไปก่อนทำหน้าที่ (ค้าง loading ตลอดไป)
|
||||
restoreHistoryApi(); // script evaluate แล้ว — office.js ลบ history ตรงนี้ คืนก่อน router ใช้
|
||||
void Office.onReady(() => {
|
||||
clearTimeout(fallbackTimer);
|
||||
restoreHistoryApi(); // handshake ของ host อาจลบซ้ำ — คืนอีกรอบก่อน mount TaskpaneApp
|
||||
resolve(true);
|
||||
});
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue