implement phase 5 และ fix bugs
This commit is contained in:
parent
f4b1c3e14b
commit
1bb850c6b8
40 changed files with 1507 additions and 429 deletions
|
|
@ -1,45 +1,430 @@
|
|||
import * as React from "react";
|
||||
import Header from "./Header";
|
||||
import HeroList, { HeroListItem } from "./HeroList";
|
||||
import TextInsertion from "./TextInsertion";
|
||||
import { makeStyles } from "@fluentui/react-components";
|
||||
import { Ribbon24Regular, LockOpen24Regular, DesignIdeas24Regular } from "@fluentui/react-icons";
|
||||
import { insertText } from "../taskpane";
|
||||
/* global Word, Office */
|
||||
|
||||
interface AppProps {
|
||||
title: string;
|
||||
}
|
||||
import * as React from "react";
|
||||
import {
|
||||
Button,
|
||||
MessageBar,
|
||||
MessageBarBody,
|
||||
MessageBarActions,
|
||||
Spinner,
|
||||
Text,
|
||||
makeStyles,
|
||||
tokens,
|
||||
} from "@fluentui/react-components";
|
||||
import { DocumentArrowUp24Regular, Save24Regular, SignOut24Regular } from "@fluentui/react-icons";
|
||||
import * as authService from "../../services/authService";
|
||||
import { AuthRequiredError } from "../../services/apiClient";
|
||||
import { getErrorMessage } from "../../services/errors";
|
||||
import * as metadataService from "../../services/metadataService";
|
||||
import { downloadVersion, getDownloadLink, getVersions } from "../../services/versioningService";
|
||||
import { saveAndVersion, startVersioning } from "../../services/versionFlow";
|
||||
import { VersionRecord } from "../../models/versionRecord";
|
||||
import ConfirmVersionDialog from "./ConfirmVersionDialog";
|
||||
import VersionHistoryPanel from "./VersionHistoryPanel";
|
||||
|
||||
/**
|
||||
* Task pane หลัก — state machine ตาม Phase 5:
|
||||
* checking → (ไม่มี token) signed-out
|
||||
* → (มี token + ไม่ใช่ tracked document) untracked
|
||||
* → (มี token + tracked document) tracking
|
||||
*
|
||||
* กติกา: ทุก async ต้อง catch — AuthRequiredError → กลับ signed-out อย่างเนียน (AC-9)
|
||||
* error อื่นแสดง MessageBar ไม่ปล่อย unhandled rejection
|
||||
*/
|
||||
|
||||
type Phase = "checking" | "signed-out" | "untracked" | "tracking";
|
||||
|
||||
/** FR-3 — ถามเอกสารทุก 30 วินาทีว่ามีแก้ไขที่ยังไม่ได้ save ไหม */
|
||||
const UNSAVED_CHECK_INTERVAL_MS = 30_000;
|
||||
|
||||
const useStyles = makeStyles({
|
||||
root: {
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
minHeight: "100vh",
|
||||
padding: tokens.spacingVerticalM,
|
||||
gap: tokens.spacingVerticalS,
|
||||
},
|
||||
header: {
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
paddingBottom: tokens.spacingVerticalS,
|
||||
borderBottom: `1px solid ${tokens.colorNeutralStroke1}`,
|
||||
},
|
||||
section: {
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "flex-start",
|
||||
gap: tokens.spacingVerticalS,
|
||||
paddingTop: tokens.spacingVerticalM,
|
||||
},
|
||||
checking: {
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
gap: tokens.spacingVerticalM,
|
||||
paddingTop: tokens.spacingVerticalXL,
|
||||
},
|
||||
});
|
||||
|
||||
const App: React.FC<AppProps> = (props: AppProps) => {
|
||||
const App: React.FC = () => {
|
||||
const styles = useStyles();
|
||||
// The list items are static and won't change at runtime,
|
||||
// so this should be an ordinary const, not a part of state.
|
||||
const listItems: HeroListItem[] = [
|
||||
{
|
||||
icon: <Ribbon24Regular />,
|
||||
primaryText: "Achieve more with Office integration",
|
||||
|
||||
const [phase, setPhase] = React.useState<Phase>("checking");
|
||||
const [bootError, setBootError] = React.useState<string | null>(null);
|
||||
const [username, setUsername] = React.useState<string | null>(null);
|
||||
|
||||
const [documentId, setDocumentId] = React.useState<string | null>(null);
|
||||
const [versions, setVersions] = React.useState<VersionRecord[]>([]);
|
||||
const [historyLoading, setHistoryLoading] = React.useState(false);
|
||||
const [historyError, setHistoryError] = React.useState<string | null>(null);
|
||||
|
||||
const [signingIn, setSigningIn] = React.useState(false);
|
||||
const [loginError, setLoginError] = React.useState<string | null>(null);
|
||||
|
||||
const [starting, setStarting] = React.useState(false);
|
||||
const [saving, setSaving] = React.useState(false);
|
||||
const [progressMessage, setProgressMessage] = React.useState<string | null>(null);
|
||||
const [confirmOpen, setConfirmOpen] = React.useState(false);
|
||||
const [flowError, setFlowError] = React.useState<string | null>(null);
|
||||
const [flowSuccess, setFlowSuccess] = React.useState<string | null>(null);
|
||||
|
||||
const [downloadingVersionId, setDownloadingVersionId] = React.useState<string | null>(null);
|
||||
const [hasUnsavedChanges, setHasUnsavedChanges] = React.useState(false);
|
||||
|
||||
/** 401 ที่ refresh ไม่ช่วย → กลับหน้า sign-in อย่างเนียน (AC-9) */
|
||||
const handleAuthRequired = React.useCallback((error: unknown): boolean => {
|
||||
if (error instanceof AuthRequiredError) {
|
||||
setPhase("signed-out");
|
||||
setDocumentId(null);
|
||||
setVersions([]);
|
||||
setLoginError("เซสชันหมดอายุ — กรุณา login ใหม่");
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}, []);
|
||||
|
||||
const refreshHistory = React.useCallback(
|
||||
async (id: string) => {
|
||||
setHistoryLoading(true);
|
||||
setHistoryError(null);
|
||||
try {
|
||||
setVersions(await getVersions(id));
|
||||
} catch (error) {
|
||||
if (!handleAuthRequired(error)) {
|
||||
setHistoryError(getErrorMessage(error, "โหลด version history ไม่สำเร็จ"));
|
||||
}
|
||||
} finally {
|
||||
setHistoryLoading(false);
|
||||
}
|
||||
},
|
||||
{
|
||||
icon: <LockOpen24Regular />,
|
||||
primaryText: "Unlock features and functionality",
|
||||
},
|
||||
{
|
||||
icon: <DesignIdeas24Regular />,
|
||||
primaryText: "Create and visualize like a pro",
|
||||
},
|
||||
];
|
||||
[handleAuthRequired]
|
||||
);
|
||||
|
||||
/** ตรวจตอนเปิด task pane (FR-1): token → SavedByAddIn → history */
|
||||
const detect = React.useCallback(async () => {
|
||||
setPhase("checking");
|
||||
setBootError(null);
|
||||
try {
|
||||
const token = await authService.getAccessToken();
|
||||
if (!token) {
|
||||
setPhase("signed-out");
|
||||
return;
|
||||
}
|
||||
authService.startAutoRefresh();
|
||||
setUsername(authService.getUsername() ?? null);
|
||||
|
||||
const tracked = await metadataService.isTrackedDocument();
|
||||
if (!tracked) {
|
||||
setPhase("untracked");
|
||||
return;
|
||||
}
|
||||
const properties = await metadataService.readTrackedProperties();
|
||||
const id = properties.documentTrackingId;
|
||||
if (!id) {
|
||||
// mark ว่า track แต่ไม่มี id (ไฟล์เสียหาย) — ให้เริ่มใหม่แบบ untracked
|
||||
setPhase("untracked");
|
||||
return;
|
||||
}
|
||||
setDocumentId(id);
|
||||
setPhase("tracking");
|
||||
await refreshHistory(id);
|
||||
} catch (error) {
|
||||
setBootError(getErrorMessage(error, "ตรวจสอบเอกสารไม่สำเร็จ"));
|
||||
}
|
||||
}, [refreshHistory]);
|
||||
|
||||
React.useEffect(() => {
|
||||
void detect();
|
||||
}, [detect]);
|
||||
|
||||
/** FR-3 — poll `document.saved` เฉพาะตอน tracking */
|
||||
React.useEffect(() => {
|
||||
if (phase !== "tracking") {
|
||||
setHasUnsavedChanges(false);
|
||||
return;
|
||||
}
|
||||
const interval = setInterval(() => {
|
||||
void (async () => {
|
||||
try {
|
||||
const unsaved = await Word.run(async (context) => {
|
||||
const document = context.document;
|
||||
document.load("saved");
|
||||
await context.sync();
|
||||
return !document.saved;
|
||||
});
|
||||
setHasUnsavedChanges(unsaved);
|
||||
} catch {
|
||||
// อ่านไม่ได้รอบนี้ก็ข้าม — banner เป็นแค่ soft reminder
|
||||
}
|
||||
})();
|
||||
}, UNSAVED_CHECK_INTERVAL_MS);
|
||||
return () => clearInterval(interval);
|
||||
}, [phase]);
|
||||
|
||||
const handleSignIn = async () => {
|
||||
setSigningIn(true);
|
||||
setLoginError(null);
|
||||
try {
|
||||
const result = await authService.login();
|
||||
if (result.ok) {
|
||||
await detect();
|
||||
} else {
|
||||
setLoginError(result.error);
|
||||
}
|
||||
} catch (error) {
|
||||
setLoginError(getErrorMessage(error, "เข้าสู่ระบบไม่สำเร็จ"));
|
||||
} finally {
|
||||
setSigningIn(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSignOut = () => {
|
||||
authService.signOut();
|
||||
setUsername(null);
|
||||
setDocumentId(null);
|
||||
setVersions([]);
|
||||
setLoginError(null);
|
||||
setPhase("signed-out");
|
||||
};
|
||||
|
||||
/** Start Versioning — stamp รอบแรก + upload v1 (FR-1/AC-5) */
|
||||
const handleStartVersioning = async () => {
|
||||
setStarting(true);
|
||||
setFlowError(null);
|
||||
setFlowSuccess(null);
|
||||
try {
|
||||
const result = await startVersioning(setProgressMessage);
|
||||
setDocumentId(result.documentId);
|
||||
setPhase("tracking");
|
||||
setFlowSuccess(`เริ่มติดตามเอกสารนี้แล้ว — สร้างเป็น version 1`);
|
||||
await refreshHistory(result.documentId);
|
||||
} catch (error) {
|
||||
if (!handleAuthRequired(error)) {
|
||||
setFlowError(getErrorMessage(error, "เริ่มติดตามเอกสารไม่สำเร็จ"));
|
||||
}
|
||||
} finally {
|
||||
setProgressMessage(null);
|
||||
setStarting(false);
|
||||
}
|
||||
};
|
||||
|
||||
/** Save & Version (FR-2) — ตามลำดับ spec: confirm → stamp → XML → save → file+fields → upload */
|
||||
const handleSaveAndVersion = async () => {
|
||||
if (!documentId) {
|
||||
return;
|
||||
}
|
||||
setSaving(true);
|
||||
setFlowError(null);
|
||||
setFlowSuccess(null);
|
||||
try {
|
||||
const latestVersionNumber = versions[0]?.versionNumber ?? 0;
|
||||
const result = await saveAndVersion(documentId, latestVersionNumber, setProgressMessage);
|
||||
setFlowSuccess(`สร้าง version ${result.versionNumber} เรียบร้อย`);
|
||||
await refreshHistory(documentId);
|
||||
} catch (error) {
|
||||
if (!handleAuthRequired(error)) {
|
||||
setFlowError(getErrorMessage(error, "สร้าง version ไม่สำเร็จ"));
|
||||
}
|
||||
} finally {
|
||||
setProgressMessage(null);
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* FR-4 — ดาวน์โหลด version ที่เลือก: ขอ presigned URL อายุสั้นจาก backend แล้วเปิดใน
|
||||
* เบราว์เซอร์ของเครื่อง (webview ของ Word — โดยเฉพาะ WKWebView บน Mac — ทิ้ง blob download
|
||||
* แบบเงียบๆ ไฟล์ไม่เคยถูกเขียนที่ไหนเลย) · host ไหนไม่มี openBrowserWindow จะย้อนกลับไปใช้ blob
|
||||
*/
|
||||
const handleDownload = async (version: VersionRecord) => {
|
||||
if (!documentId) {
|
||||
return;
|
||||
}
|
||||
setDownloadingVersionId(version.versionId);
|
||||
setFlowError(null);
|
||||
try {
|
||||
const link = await getDownloadLink(documentId, version.versionId);
|
||||
if (typeof Office.context.ui.openBrowserWindow === "function") {
|
||||
Office.context.ui.openBrowserWindow(link.url);
|
||||
setFlowSuccess(`กำลังดาวน์โหลด v${version.versionNumber} ผ่านเบราว์เซอร์ของคุณ — ดูไฟล์ที่โฟลเดอร์ Downloads`);
|
||||
} else {
|
||||
const blob = await downloadVersion(documentId, version.versionId);
|
||||
const url = URL.createObjectURL(blob);
|
||||
const anchor = document.createElement("a");
|
||||
anchor.href = url;
|
||||
anchor.download = `document_v${version.versionNumber}.docx`;
|
||||
document.body.appendChild(anchor);
|
||||
anchor.click();
|
||||
anchor.remove();
|
||||
window.setTimeout(() => URL.revokeObjectURL(url), 60_000);
|
||||
}
|
||||
} catch (error) {
|
||||
if (!handleAuthRequired(error)) {
|
||||
setFlowError(getErrorMessage(error, "ดาวน์โหลด version ไม่สำเร็จ"));
|
||||
}
|
||||
} finally {
|
||||
setDownloadingVersionId(null);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.root}>
|
||||
<Header logo="assets/logo-filled.png" title={props.title} message="Welcome" />
|
||||
<HeroList message="Discover what this add-in can do for you today!" items={listItems} />
|
||||
<TextInsertion insertText={insertText} />
|
||||
<div className={styles.header}>
|
||||
<Text weight="semibold">e-Court Documents Versioning</Text>
|
||||
{phase !== "signed-out" && phase !== "checking" && username ? (
|
||||
<Button
|
||||
size="small"
|
||||
appearance="subtle"
|
||||
icon={<SignOut24Regular />}
|
||||
onClick={handleSignOut}
|
||||
>
|
||||
{username} · ออกจากระบบ
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{phase === "checking" ? (
|
||||
<div className={styles.checking}>
|
||||
<Spinner label="กำลังตรวจสอบเอกสาร…" />
|
||||
{bootError ? (
|
||||
<MessageBar intent="error">
|
||||
<MessageBarBody>{bootError}</MessageBarBody>
|
||||
<MessageBarActions>
|
||||
<Button size="small" appearance="outline" onClick={() => void detect()}>
|
||||
ลองใหม่
|
||||
</Button>
|
||||
</MessageBarActions>
|
||||
</MessageBar>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{phase === "signed-out" ? (
|
||||
<div className={styles.section}>
|
||||
<Text size={300}>เข้าสู่ระบบเพื่อเริ่มใช้งาน versioning</Text>
|
||||
<Text size={200} style={{ color: tokens.colorNeutralForeground3 }}>
|
||||
ใช้บัญชีของ realm <code>word-versioning</code> — หน้าต่าง login จะเปิดแยกต่างหาก
|
||||
</Text>
|
||||
<Button appearance="primary" disabled={signingIn} onClick={() => void handleSignIn()}>
|
||||
{signingIn ? "กำลังเข้าสู่ระบบ…" : "Sign in"}
|
||||
</Button>
|
||||
{loginError ? (
|
||||
<MessageBar intent="warning">
|
||||
<MessageBarBody>{loginError}</MessageBarBody>
|
||||
</MessageBar>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{phase === "untracked" ? (
|
||||
<div className={styles.section}>
|
||||
<Text size={300}>เอกสารนี้ยังไม่ได้ติดตาม version</Text>
|
||||
<Text size={200} style={{ color: tokens.colorNeutralForeground3 }}>
|
||||
กดปุ่มด้านล่างเพื่อเริ่ม track — ระบบจะ stamp metadata ลงไฟล์และอัปโหลดเป็น version 1
|
||||
(ไฟล์ที่สร้างจากเว็บ portal จะถูกตรวจพบและข้ามขั้นตอนนี้อัตโนมัติ)
|
||||
</Text>
|
||||
<Button
|
||||
appearance="primary"
|
||||
icon={<DocumentArrowUp24Regular />}
|
||||
disabled={starting}
|
||||
onClick={() => void handleStartVersioning()}
|
||||
>
|
||||
{starting ? "กำลังเริ่มติดตาม…" : "Start Versioning This Document"}
|
||||
</Button>
|
||||
{progressMessage ? (
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 8 }}>
|
||||
<Spinner size="tiny" />
|
||||
<Text size={200}>{progressMessage}</Text>
|
||||
</div>
|
||||
) : null}
|
||||
{flowError ? (
|
||||
<MessageBar intent="error">
|
||||
<MessageBarBody>{flowError}</MessageBarBody>
|
||||
</MessageBar>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{phase === "tracking" ? (
|
||||
<div className={styles.section}>
|
||||
{hasUnsavedChanges ? (
|
||||
<MessageBar intent="warning">
|
||||
<MessageBarBody>
|
||||
มีการแก้ไขที่ยังไม่ได้ version — กด Save & Version เพื่อบันทึกเป็น version ใหม่
|
||||
</MessageBarBody>
|
||||
</MessageBar>
|
||||
) : null}
|
||||
|
||||
{flowSuccess ? (
|
||||
<MessageBar intent="success">
|
||||
<MessageBarBody>{flowSuccess}</MessageBarBody>
|
||||
</MessageBar>
|
||||
) : null}
|
||||
{flowError ? (
|
||||
<MessageBar intent="error">
|
||||
<MessageBarBody>{flowError}</MessageBarBody>
|
||||
</MessageBar>
|
||||
) : null}
|
||||
|
||||
<Button
|
||||
appearance="primary"
|
||||
size="large"
|
||||
icon={<Save24Regular />}
|
||||
disabled={saving}
|
||||
onClick={() => setConfirmOpen(true)}
|
||||
>
|
||||
Save & Version
|
||||
</Button>
|
||||
{saving && progressMessage ? (
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 8 }}>
|
||||
<Spinner size="tiny" />
|
||||
<Text size={200}>{progressMessage}</Text>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<Text size={200} style={{ color: tokens.colorNeutralForeground3 }}>
|
||||
tracking id: {documentId?.slice(0, 8)}…
|
||||
</Text>
|
||||
|
||||
<VersionHistoryPanel
|
||||
versions={versions}
|
||||
loading={historyLoading}
|
||||
error={historyError}
|
||||
downloadingVersionId={downloadingVersionId}
|
||||
onDownload={(version) => void handleDownload(version)}
|
||||
onRetry={() => documentId && void refreshHistory(documentId)}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<ConfirmVersionDialog
|
||||
open={confirmOpen}
|
||||
busy={saving}
|
||||
onConfirm={() => void handleSaveAndVersion()}
|
||||
onDismiss={() => setConfirmOpen(false)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
55
addin/src/taskpane/components/ConfirmVersionDialog.tsx
Normal file
55
addin/src/taskpane/components/ConfirmVersionDialog.tsx
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
import * as React from "react";
|
||||
import {
|
||||
Button,
|
||||
Dialog,
|
||||
DialogActions,
|
||||
DialogBody,
|
||||
DialogContent,
|
||||
DialogSurface,
|
||||
DialogTitle,
|
||||
Spinner,
|
||||
} from "@fluentui/react-components";
|
||||
|
||||
interface ConfirmVersionDialogProps {
|
||||
open: boolean;
|
||||
/** กำลังอยู่ระหว่าง flow หลังกดยืนยันแล้ว — ปุ่นทั้งสอง disable + แสดง spinner */
|
||||
busy: boolean;
|
||||
onConfirm: () => void;
|
||||
onDismiss: () => void;
|
||||
}
|
||||
|
||||
/** FR-2 ขั้นตอนที่ 1 — dialog ยืนยัน "สร้าง version ใหม่ของเอกสารนี้หรือไม่?" */
|
||||
export const ConfirmVersionDialog: React.FC<ConfirmVersionDialogProps> = ({
|
||||
open,
|
||||
busy,
|
||||
onConfirm,
|
||||
onDismiss,
|
||||
}) => {
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={(_event, data) => !data.open && !busy && onDismiss()} modalType="alert">
|
||||
<DialogSurface>
|
||||
<DialogBody>
|
||||
<DialogTitle>สร้าง version ใหม่?</DialogTitle>
|
||||
<DialogContent>
|
||||
ระบบจะบันทึกไฟล์ปัจจุบันและอัปโหลดเป็น version ถัดไปของเอกสารนี้
|
||||
{busy ? (
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 8, marginTop: 12 }}>
|
||||
<Spinner size="tiny" /> กำลังดำเนินการ…
|
||||
</div>
|
||||
) : null}
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button appearance="secondary" disabled={busy} onClick={onDismiss}>
|
||||
ยกเลิก
|
||||
</Button>
|
||||
<Button appearance="primary" disabled={busy} onClick={onConfirm}>
|
||||
สร้าง version
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</DialogBody>
|
||||
</DialogSurface>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default ConfirmVersionDialog;
|
||||
|
|
@ -1,38 +0,0 @@
|
|||
import * as React from "react";
|
||||
import { Image, tokens, makeStyles } from "@fluentui/react-components";
|
||||
|
||||
export interface HeaderProps {
|
||||
title: string;
|
||||
logo: string;
|
||||
message: string;
|
||||
}
|
||||
|
||||
const useStyles = makeStyles({
|
||||
welcome__header: {
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
paddingBottom: "30px",
|
||||
paddingTop: "100px",
|
||||
backgroundColor: tokens.colorNeutralBackground3,
|
||||
},
|
||||
message: {
|
||||
fontSize: tokens.fontSizeHero900,
|
||||
fontWeight: tokens.fontWeightRegular,
|
||||
fontColor: tokens.colorNeutralBackgroundStatic,
|
||||
},
|
||||
});
|
||||
|
||||
const Header: React.FC<HeaderProps> = (props: HeaderProps) => {
|
||||
const { title, logo, message } = props;
|
||||
const styles = useStyles();
|
||||
|
||||
return (
|
||||
<section className={styles.welcome__header}>
|
||||
<Image width="90" height="90" src={logo} alt={title} />
|
||||
<h1 className={styles.message}>{message}</h1>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default Header;
|
||||
|
|
@ -1,62 +0,0 @@
|
|||
import * as React from "react";
|
||||
import { tokens, makeStyles } from "@fluentui/react-components";
|
||||
|
||||
export interface HeroListItem {
|
||||
icon: React.JSX.Element;
|
||||
primaryText: string;
|
||||
}
|
||||
|
||||
export interface HeroListProps {
|
||||
message: string;
|
||||
items: HeroListItem[];
|
||||
}
|
||||
|
||||
const useStyles = makeStyles({
|
||||
list: {
|
||||
marginTop: "20px",
|
||||
},
|
||||
listItem: {
|
||||
paddingBottom: "20px",
|
||||
display: "flex",
|
||||
},
|
||||
icon: {
|
||||
marginRight: "10px",
|
||||
},
|
||||
itemText: {
|
||||
fontSize: tokens.fontSizeBase300,
|
||||
fontColor: tokens.colorNeutralBackgroundStatic,
|
||||
},
|
||||
welcome__main: {
|
||||
width: "100%",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
},
|
||||
message: {
|
||||
fontSize: tokens.fontSizeBase500,
|
||||
fontColor: tokens.colorNeutralBackgroundStatic,
|
||||
fontWeight: tokens.fontWeightRegular,
|
||||
paddingLeft: "10px",
|
||||
paddingRight: "10px",
|
||||
},
|
||||
});
|
||||
|
||||
const HeroList: React.FC<HeroListProps> = (props: HeroListProps) => {
|
||||
const { items, message } = props;
|
||||
const styles = useStyles();
|
||||
|
||||
const listItems = items.map((item, index) => (
|
||||
<li className={styles.listItem} key={index}>
|
||||
<i className={styles.icon}>{item.icon}</i>
|
||||
<span className={styles.itemText}>{item.primaryText}</span>
|
||||
</li>
|
||||
));
|
||||
return (
|
||||
<div className={styles.welcome__main}>
|
||||
<h2 className={styles.message}>{message}</h2>
|
||||
<ul className={styles.list}>{listItems}</ul>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default HeroList;
|
||||
|
|
@ -1,57 +0,0 @@
|
|||
import * as React from "react";
|
||||
import { useState } from "react";
|
||||
import { Button, Field, Textarea, tokens, makeStyles } from "@fluentui/react-components";
|
||||
|
||||
/* global HTMLTextAreaElement */
|
||||
|
||||
interface TextInsertionProps {
|
||||
insertText: (_text: string) => void;
|
||||
}
|
||||
|
||||
const useStyles = makeStyles({
|
||||
instructions: {
|
||||
fontWeight: tokens.fontWeightSemibold,
|
||||
marginTop: "20px",
|
||||
marginBottom: "10px",
|
||||
},
|
||||
textPromptAndInsertion: {
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
},
|
||||
textAreaField: {
|
||||
marginLeft: "20px",
|
||||
marginTop: "30px",
|
||||
marginBottom: "20px",
|
||||
marginRight: "20px",
|
||||
maxWidth: "50%",
|
||||
},
|
||||
});
|
||||
|
||||
const TextInsertion: React.FC<TextInsertionProps> = (props: TextInsertionProps) => {
|
||||
const [text, setText] = useState<string>("Some text.");
|
||||
|
||||
const handleTextInsertion = async () => {
|
||||
await props.insertText(text);
|
||||
};
|
||||
|
||||
const handleTextChange = async (event: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
setText(event.target.value);
|
||||
};
|
||||
|
||||
const styles = useStyles();
|
||||
|
||||
return (
|
||||
<div className={styles.textPromptAndInsertion}>
|
||||
<Field className={styles.textAreaField} size="large" label="Enter text to be inserted into the document.">
|
||||
<Textarea size="large" value={text} onChange={handleTextChange} />
|
||||
</Field>
|
||||
<Field className={styles.instructions}>Click the button to insert text.</Field>
|
||||
<Button appearance="primary" disabled={false} size="large" onClick={handleTextInsertion}>
|
||||
Insert text
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TextInsertion;
|
||||
128
addin/src/taskpane/components/VersionHistoryPanel.tsx
Normal file
128
addin/src/taskpane/components/VersionHistoryPanel.tsx
Normal file
|
|
@ -0,0 +1,128 @@
|
|||
import * as React from "react";
|
||||
import {
|
||||
Badge,
|
||||
Button,
|
||||
MessageBar,
|
||||
MessageBarBody,
|
||||
Spinner,
|
||||
Text,
|
||||
makeStyles,
|
||||
tokens,
|
||||
} from "@fluentui/react-components";
|
||||
import { ArrowDownload24Regular, History24Regular } from "@fluentui/react-icons";
|
||||
import { VersionRecord } from "../../models/versionRecord";
|
||||
|
||||
interface VersionHistoryPanelProps {
|
||||
versions: VersionRecord[];
|
||||
loading: boolean;
|
||||
error: string | null;
|
||||
/** versionId ของ version ที่กำลังดาวน์โหลดอยู่ (แสดง spinner ที่ปุ่ม) */
|
||||
downloadingVersionId: string | null;
|
||||
onDownload: (version: VersionRecord) => void;
|
||||
onRetry: () => void;
|
||||
}
|
||||
|
||||
const useStyles = makeStyles({
|
||||
root: {
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: tokens.spacingVerticalS,
|
||||
},
|
||||
header: {
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: tokens.spacingHorizontalXS,
|
||||
marginTop: tokens.spacingVerticalL,
|
||||
marginBottom: tokens.spacingVerticalXXS,
|
||||
},
|
||||
item: {
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
gap: tokens.spacingHorizontalS,
|
||||
padding: `${tokens.spacingVerticalXS} ${tokens.spacingHorizontalS}`,
|
||||
borderRadius: tokens.borderRadiusMedium,
|
||||
":hover": {
|
||||
backgroundColor: tokens.colorNeutralBackground1Hover,
|
||||
},
|
||||
},
|
||||
itemInfo: {
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
minWidth: 0,
|
||||
},
|
||||
metaRow: {
|
||||
color: tokens.colorNeutralForeground3,
|
||||
fontSize: tokens.fontSizeBase200,
|
||||
},
|
||||
});
|
||||
|
||||
function formatTimestamp(iso: string): string {
|
||||
const date = new Date(iso);
|
||||
return Number.isNaN(date.getTime())
|
||||
? iso
|
||||
: date.toLocaleString(undefined, { dateStyle: "medium", timeStyle: "short" });
|
||||
}
|
||||
|
||||
/** FR-4 — แสดง version history (เลข version / เวลา / ผู้สร้าง) + ปุ่มดาวน์โหลด version นั้น */
|
||||
export const VersionHistoryPanel: React.FC<VersionHistoryPanelProps> = ({
|
||||
versions,
|
||||
loading,
|
||||
error,
|
||||
downloadingVersionId,
|
||||
onDownload,
|
||||
onRetry,
|
||||
}) => {
|
||||
const styles = useStyles();
|
||||
|
||||
return (
|
||||
<div className={styles.root}>
|
||||
<div className={styles.header}>
|
||||
<History24Regular />
|
||||
<Text weight="semibold">Version history</Text>
|
||||
</div>
|
||||
|
||||
{loading ? <Spinner size="small" label="กำลังโหลด…" /> : null}
|
||||
|
||||
{error ? (
|
||||
<MessageBar intent="error">
|
||||
<MessageBarBody>
|
||||
{error}{" "}
|
||||
<Button size="small" appearance="subtle" onClick={onRetry}>
|
||||
ลองใหม่
|
||||
</Button>
|
||||
</MessageBarBody>
|
||||
</MessageBar>
|
||||
) : null}
|
||||
|
||||
{!loading && !error && versions.length === 0 ? (
|
||||
<Text size={200} style={{ color: tokens.colorNeutralForeground3 }}>
|
||||
ยังไม่มี version ในระบบ
|
||||
</Text>
|
||||
) : null}
|
||||
|
||||
{versions.map((version) => (
|
||||
<div className={styles.item} key={version.versionId}>
|
||||
<div className={styles.itemInfo}>
|
||||
<Badge appearance="filled" color="brand">
|
||||
v{version.versionNumber}
|
||||
</Badge>
|
||||
<span className={styles.metaRow}>{formatTimestamp(version.createdAt)}</span>
|
||||
<span className={styles.metaRow}>โดย {version.createdByUserId}</span>
|
||||
</div>
|
||||
<Button
|
||||
icon={<ArrowDownload24Regular />}
|
||||
size="small"
|
||||
appearance="subtle"
|
||||
disabled={downloadingVersionId !== null}
|
||||
onClick={() => onDownload(version)}
|
||||
>
|
||||
{downloadingVersionId === version.versionId ? "กำลังดาวน์โหลด…" : "ดาวน์โหลด"}
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default VersionHistoryPanel;
|
||||
|
|
@ -5,8 +5,6 @@ import { FluentProvider, webLightTheme } from "@fluentui/react-components";
|
|||
|
||||
/* global document, Office, module, require, HTMLElement */
|
||||
|
||||
const title = "Contoso Task Pane Add-in";
|
||||
|
||||
const rootElement: HTMLElement | null = document.getElementById("container");
|
||||
const root = rootElement ? createRoot(rootElement) : undefined;
|
||||
|
||||
|
|
@ -14,7 +12,7 @@ const root = rootElement ? createRoot(rootElement) : undefined;
|
|||
Office.onReady(() => {
|
||||
root?.render(
|
||||
<FluentProvider theme={webLightTheme}>
|
||||
<App title={title} />
|
||||
<App />
|
||||
</FluentProvider>
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
<meta charset="UTF-8" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=Edge" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Contoso Task Pane Add-in</title>
|
||||
<title>e-Court Documents Versioning</title>
|
||||
|
||||
<!-- Office JavaScript API -->
|
||||
<script type="text/javascript" src="https://appsforoffice.microsoft.com/lib/1/hosted/office.js"></script>
|
||||
|
|
|
|||
|
|
@ -1,14 +0,0 @@
|
|||
/* global Word console */
|
||||
|
||||
export async function insertText(text: string) {
|
||||
// Write text to the document.
|
||||
try {
|
||||
await Word.run(async (context) => {
|
||||
let body = context.document.body;
|
||||
body.insertParagraph(text, Word.InsertLocation.end);
|
||||
await context.sync();
|
||||
});
|
||||
} catch (error) {
|
||||
console.log("Error: " + error);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue