Skip to content

Commit d2dfd5c

Browse files
committed
feat(web): preview docx, xlsx, and pptx files
1 parent b727408 commit d2dfd5c

14 files changed

Lines changed: 750 additions & 7 deletions

‎apps/server/src/assets/AssetAccess.test.ts‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -922,7 +922,13 @@ describe("AssetAccess", () => {
922922
const directory = path.join(root, "assets#archive");
923923
yield* fileSystem.makeDirectory(directory);
924924

925-
for (const name of ["icon#v2.png", "report#draft.pdf"]) {
925+
for (const name of [
926+
"icon#v2.png",
927+
"report#draft.pdf",
928+
"report.docx",
929+
"workbook.xlsx",
930+
"slides.pptx",
931+
]) {
926932
const filePath = path.join(directory, name);
927933
yield* fileSystem.writeFileString(filePath, "preview fixture");
928934
const canonicalFile = yield* fileSystem.realPath(filePath);

‎apps/server/src/assets/AssetAccess.ts‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,9 @@ const inlinePreviewMimeTypeForExtension = (extension: string) =>
8787
const PREVIEW_ASSET_EXTENSIONS = new Set([
8888
...WORKSPACE_BROWSER_PREVIEW_EXTENSIONS,
8989
...WORKSPACE_IMAGE_PREVIEW_EXTENSIONS,
90+
".docx",
91+
".xlsx",
92+
".pptx",
9093
".css",
9194
".js",
9295
".mjs",

‎apps/web/package.json‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
},
1414
"dependencies": {
1515
"@base-ui/react": "^1.4.1",
16+
"@betteroffice/fonts": "0.4.0",
17+
"@betteroffice/pptx": "0.2.0",
1618
"@clerk/clerk-js": "catalog:",
1719
"@clerk/electron": "catalog:",
1820
"@clerk/react": "catalog:",
@@ -53,6 +55,7 @@
5355
"@tiptap/starter-kit": "^3.31.3",
5456
"class-variance-authority": "^0.7.1",
5557
"culori": "^4.0.2",
58+
"docx-preview": "0.4.1",
5659
"dompurify": "^3.4.16",
5760
"effect": "catalog:",
5861
"hast-util-to-html": "^9.0.5",
@@ -68,6 +71,7 @@
6871
"react": "19.2.6",
6972
"react-dom": "19.2.6",
7073
"react-markdown": "^10.1.0",
74+
"read-excel-file": "9.3.12",
7175
"remark-gfm": "catalog:",
7276
"tailwind-merge": "^3.4.0",
7377
"unbash": "^5.0.0",

‎apps/web/src/components/files/AttachmentFilePreview.tsx‎

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,11 @@ import { readFilePreviewResponse } from "@t3tools/client-runtime/file-preview";
55
import { settlePromise } from "@t3tools/client-runtime/state/runtime";
66
import * as AsyncResult from "effect/reactivity/AsyncResult";
77
import * as Cause from "effect/Cause";
8-
import { filePreviewKind, FILE_TEXT_PREVIEW_MAX_BYTES } from "@t3tools/shared/filePreview";
8+
import {
9+
filePreviewKind,
10+
officePreviewKind,
11+
FILE_TEXT_PREVIEW_MAX_BYTES,
12+
} from "@t3tools/shared/filePreview";
913
import { ChevronRightIcon, DownloadIcon, Trash2Icon, WrapTextIcon, XIcon } from "lucide-react";
1014
import { Check, Code2, Copy, Eye, Table2 } from "lucide";
1115
import { lazy, Suspense, useEffect, useRef, useState } from "react";
@@ -22,6 +26,7 @@ import { cn } from "~/lib/utils";
2226
import { AudioPreview } from "./AudioPreview";
2327
import { BrowserDocumentFrame } from "./BrowserDocumentFrame";
2428
import { DelimitedTablePreview } from "./DelimitedTablePreview";
29+
import { OfficeFilePreview } from "./OfficeFilePreview";
2530
import {
2631
FILE_SURFACE_SUBHEADER_CLASS,
2732
FileSurfaceAction,
@@ -61,6 +66,7 @@ export function AttachmentFilePreview(props: {
6166
onClose?: () => void;
6267
}) {
6368
const kind = filePreviewKind(props);
69+
const officeKind = officePreviewKind(props);
6470
const delimiter = filePreviewDelimiter(props);
6571
const renderedMode =
6672
kind === "markdown" ? "markdown" : kind === "html" ? "html" : delimiter ? "table" : null;
@@ -219,6 +225,8 @@ export function AttachmentFilePreview(props: {
219225
) : (
220226
<ReadOnlySourcePreview name={props.name} text={content.text} />
221227
)
228+
) : officeKind ? (
229+
<OfficeFilePreview src={url} name={props.name} kind={officeKind} sizeBytes={props.sizeBytes} />
222230
) : kind === "pdf" || kind === "html" ? (
223231
<BrowserDocumentFrame
224232
src={url}

‎apps/web/src/components/files/FilePreviewPanel.tsx‎

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
isWorkspaceAudioPreviewPath,
1313
isWorkspaceImagePreviewPath,
1414
isWorkspaceVideoPreviewPath,
15+
officePreviewKind,
1516
} from "@t3tools/shared/filePreview";
1617
import {
1718
DEFAULT_TOKENIZE_MAX_LENGTH,
@@ -75,6 +76,7 @@ import { AttachmentFilePreview } from "./AttachmentFilePreview";
7576
import { AudioPreview } from "./AudioPreview";
7677
import { BrowserDocumentFrame, isPdfPreviewFile } from "./BrowserDocumentFrame";
7778
import { DelimitedTablePreview } from "./DelimitedTablePreview";
79+
import { OfficeFilePreview } from "./OfficeFilePreview";
7880
import FileBrowserPanel from "./FileBrowserPanel";
7981
import { FileBreadcrumbs } from "./FileBreadcrumbs";
8082
import { breadcrumbPathOf, retainedBreadcrumbTrail, trailChildOf } from "./filePath";
@@ -251,6 +253,15 @@ function WorkspaceBrowserPreview(props: {
251253
</div>
252254
);
253255
}
256+
const officeKind = officePreviewKind({ name: props.absolutePath });
257+
if (officeKind)
258+
return (
259+
<OfficeFilePreview
260+
src={`${assetUrl.url}${revisionSuffix}`}
261+
name={props.title}
262+
kind={officeKind}
263+
/>
264+
);
254265
return (
255266
<BrowserDocumentFrame
256267
src={`${assetUrl.url}${revisionSuffix}`}
@@ -1004,6 +1015,7 @@ export default function FilePreviewPanel({
10041015
const isImage = relativePath !== null && !isVideo && isWorkspaceImagePreviewPath(relativePath);
10051016
const isMedia = isImage || isVideo || isAudio;
10061017
const isPdf = relativePath !== null && isPdfPreviewFile(relativePath);
1018+
const isOffice = relativePath !== null && officePreviewKind({ name: relativePath }) !== null;
10071019
const isHtml = relativePath !== null && !isPdf && isBrowserPreviewFile(relativePath);
10081020
const isHostFile =
10091021
attachment !== undefined || (relativePath !== null && isAbsolutePath(relativePath));
@@ -1089,7 +1101,8 @@ export default function FilePreviewPanel({
10891101
file.data !== null &&
10901102
!(isMarkdown && renderMarkdown) &&
10911103
!(tableDelimiter && renderTable) &&
1092-
!renderBrowserFile;
1104+
!renderBrowserFile &&
1105+
!isOffice;
10931106
const rendered = isMarkdown ? renderMarkdown : tableDelimiter ? renderTable : renderBrowserFile;
10941107
const setRenderedPreferred = isMarkdown
10951108
? setRenderMarkdownPreferred
@@ -1126,7 +1139,7 @@ export default function FilePreviewPanel({
11261139
// Media and PDFs never show their contents, so re-reading them on every
11271140
// workspace mutation is waste. A folder named like one still re-reads, so
11281141
// it notices when the path becomes a file.
1129-
(isFolder || (!isMedia && !isPdf)) &&
1142+
(isFolder || (!isMedia && !isPdf && !isOffice)) &&
11301143
!selectedFilePending,
11311144
mutationId: workspaceMutationId,
11321145
refresh: file.refresh,
@@ -1337,7 +1350,7 @@ export default function FilePreviewPanel({
13371350
alt={relativePath}
13381351
workspaceMutationId={workspaceMutationId}
13391352
/>
1340-
) : relativePath && renderBrowserFile && absolutePath ? (
1353+
) : relativePath && (renderBrowserFile || isOffice) && absolutePath ? (
13411354
<WorkspaceBrowserPreview
13421355
key={absolutePath}
13431356
environmentId={environmentId}
@@ -1448,7 +1461,7 @@ export default function FilePreviewPanel({
14481461
selectedPathRevealId={revealRequestId}
14491462
onOpenFile={onOpenFile}
14501463
workspaceMutationId={workspaceMutationId}
1451-
{...(previewPath && !isMedia && !isPdf
1464+
{...(previewPath && !isMedia && !isPdf && !isOffice
14521465
? { onRefreshSelectedFile: file.refresh }
14531466
: {})}
14541467
/>
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import type { OfficePreviewKind } from "@t3tools/shared/filePreview";
2+
import { lazy, Suspense } from "react";
3+
4+
import { FileSurfaceLoading } from "./fileSurfaceChrome";
5+
6+
const OfficeViewer = lazy(() => import("./OfficeViewer"));
7+
8+
export function OfficeFilePreview(props: {
9+
readonly src: string;
10+
readonly name: string;
11+
readonly kind: OfficePreviewKind;
12+
readonly sizeBytes?: number;
13+
}) {
14+
return (
15+
<Suspense fallback={<FileSurfaceLoading />}>
16+
<OfficeViewer key={props.src} {...props} />
17+
</Suspense>
18+
);
19+
}

0 commit comments

Comments
 (0)