From 58651b8359e7fcb5eefe9fa7f427cb80c3730ccc Mon Sep 17 00:00:00 2001 From: "HYD@OVERTON" Date: Tue, 22 Sep 2026 14:47:01 +0800 Subject: [PATCH] fix(web): resize Excel previews with their pane --- apps/web/tests/document-preview.e2e.ts | 52 +++++++++++++++++++ .../README.i18n.yaml | 4 +- .../ui-sidebar-documentpreview/README.md | 2 +- .../ui-sidebar-documentpreview/README.zh.md | 2 +- .../src/client/excel/excel.tsx | 14 ++++- .../tests/excel-body.client.spec.tsx | 28 +++++++++- 6 files changed, 94 insertions(+), 8 deletions(-) diff --git a/apps/web/tests/document-preview.e2e.ts b/apps/web/tests/document-preview.e2e.ts index 2a2a2ba60d..6df615cec2 100644 --- a/apps/web/tests/document-preview.e2e.ts +++ b/apps/web/tests/document-preview.e2e.ts @@ -104,6 +104,58 @@ async function hideDocumentZoom(page: Page, preview: Locator): Promise { await expect.poll(() => preview.locator('[data-document-zoom-controls]').getAttribute('data-document-zoom-visible')).toBeNull() } +it.skipIf(MODE === 'record')('resizes the spreadsheet canvas with its pane while retaining the selected cell', async () => { + const scaffold = await launchWebScaffold({ replayFixture: FIXTURE, paceMs: 5, compareReplaySession: false }) + let browser: Browser | undefined + try { + browser = await chromium.launch() + const page = await newEnglishPage(browser) + onTestFailed(async () => { await saveFailureShot(page, `screenshots/0908-document-preview/excel-resize-${process.pid}`) }) + await page.goto(scaffold.authenticatedUrl, { waitUntil: 'load' }) + await connectFreshWorkspace(page, scaffold.workspaceCwd) + const settled = scaffold.whenTurnSettled() + const input = page.locator('[data-composer-input]').first() + await input.fill(PROMPT) + await input.press('Enter') + const sessionId = await settled + const cwd = scaffold.ctx.agents.get(sessionId)?.session.header.cwd + if (cwd === undefined) throw new Error('settled Session has no workspace cwd') + await writeFile(join(cwd, 'budget.xlsx'), await excelFixture()) + await page.getByText('LIGHTHOUSE', { exact: true }).waitFor() + const column = page.locator('[data-rightbar-col]') + await page.locator('[data-sidebar-right-expand]').click() + await column.locator('[data-sidebar-right-guide-entry="files"]').click() + await column.locator('[data-files-state="tree"]').waitFor({ state: 'visible' }) + await column.locator('[data-files-entry="file"]').getByRole('button', { name: 'budget.xlsx', exact: true }).click() + const excel = column.locator('[data-excel-preview]') + await excel.locator('.luckysheet-sheets-item-name').getByText('公式与格式', { exact: true }).click() + await excel.locator('.fortune-sheet-overlay').click({ position: { x: 140, y: 30 } }) + const formula = excel.locator('.fortune-fx-input') + await expect.poll(() => formula.innerText()).toBe('46281') + const canvas = excel.locator('canvas').first() + const originalCanvas = await canvas.elementHandle() + const original = await canvas.boundingBox() + if (original === null || originalCanvas === null) throw new Error('spreadsheet canvas is unavailable') + const panel = page.locator('[data-sidebar-right-panel]') + const panelBounds = await panel.boundingBox() + if (panelBounds === null) throw new Error('sidebar panel is unavailable') + const viewport = page.viewportSize() + const layout = await page.addStyleTag({ content: `[data-sidebar-right-panel] { width: ${panelBounds.width + 160}px !important; }` }) + await expect.poll(async () => Math.round((await canvas.boundingBox())!.width - original.width)).toBe(160) + await expect.poll(() => formula.innerText()).toBe('46281') + expect(await originalCanvas.evaluate(node => node.isConnected)).toBe(true) + expect(page.viewportSize()).toEqual(viewport) + await successShot(page, 'excel-resize-wide') + await layout.evaluate((node) => { node.textContent = '' }) + await expect.poll(async () => Math.round((await canvas.boundingBox())!.width)).toBe(Math.round(original.width)) + expect(await formula.innerText()).toBe('46281') + await successShot(page, 'excel-resize-restored') + await originalCanvas.dispose() + } finally { + try { await browser?.close() } finally { await scaffold.close() } + } +}) + describe.skipIf(MODE === 'record')('web e2e: document preview through Files', () => { let scaffold: WebScaffold let browser: Browser diff --git a/packages/client/ui-sidebar-documentpreview/README.i18n.yaml b/packages/client/ui-sidebar-documentpreview/README.i18n.yaml index 0978246a14..a72cc9689e 100644 --- a/packages/client/ui-sidebar-documentpreview/README.i18n.yaml +++ b/packages/client/ui-sidebar-documentpreview/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/client/ui-sidebar-documentpreview/README.md -README.md: 224957fec45fd4d2849df677b9cada62d01d18cd -README.zh.md: 18fe9e94617291d85fd3db7bf1c7bcc347f6ec73 +README.md: c238b30dd87a72d4a2a0ed4f29498f72c32c61b8 +README.zh.md: 559f6e337c3aadd29d3551f64c59307073461808 diff --git a/packages/client/ui-sidebar-documentpreview/README.md b/packages/client/ui-sidebar-documentpreview/README.md index 224957fec4..c238b30dd8 100644 --- a/packages/client/ui-sidebar-documentpreview/README.md +++ b/packages/client/ui-sidebar-documentpreview/README.md @@ -66,7 +66,7 @@ Initial reads, additional pages, and HTML/PDF/image preparation share the ongoin ## Excel preview -Open `.xlsx`, `.xls`, `.csv`, and `.tsv` directly in the browser with worksheet tabs, cell selection, copying, and a read-only formula bar. XLSX retains fonts, solid fills, borders, number formats, rich text, merged cells, row and column sizes, hidden rows/columns/sheets, and frozen headings. XLS retains saved values, formulas, number formats, merges, and available row/column metadata; fonts, borders, and frozen panes are unsupported. Workbooks display saved formula results without recalculating; missing results remain blank, and a compact formula-bar warning marks workbooks whose displayed results may be incomplete or inaccurate. Spreadsheet preview does not call the Office conversion service. +Open `.xlsx`, `.xls`, `.csv`, and `.tsv` directly in the browser with worksheet tabs, cell selection, copying, and a read-only formula bar. XLSX retains fonts, solid fills, borders, number formats, rich text, merged cells, row and column sizes, hidden rows/columns/sheets, and frozen headings. XLS retains saved values, formulas, number formats, merges, and available row/column metadata; fonts, borders, and frozen panes are unsupported. Workbooks display saved formula results without recalculating; missing results remain blank, and a compact formula-bar warning marks workbooks whose displayed results may be incomplete or inaccurate. The workbook viewport follows preview pane size changes without reloading its sheets or selection. Spreadsheet preview does not call the Office conversion service. XLSX preview omits DrawingML parts from an in-memory copy before parsing and ignores worksheet drawing references. The original file and worksheet XML remain unchanged. A notice above the table lists detected charts, images, shapes, and conditional formatting that are not displayed, and recommends opening the workbook in a system application. Files without these detected features have no notice; formula warnings remain separate. diff --git a/packages/client/ui-sidebar-documentpreview/README.zh.md b/packages/client/ui-sidebar-documentpreview/README.zh.md index 18fe9e9461..559f6e337c 100644 --- a/packages/client/ui-sidebar-documentpreview/README.zh.md +++ b/packages/client/ui-sidebar-documentpreview/README.zh.md @@ -66,7 +66,7 @@ PNG、JPEG、GIF、WebP、BMP、ICO 和 SVG 通过 Blob URL 在 `` 静态 ## Excel 预览 -直接在浏览器中打开 `.xlsx`、`.xls`、`.csv` 和 `.tsv`,支持工作表标签、单元格选择、复制和只读公式栏。XLSX 保留字体、纯色填充、边框、数字格式、富文本、合并单元格、行列尺寸、隐藏行列及工作表,以及冻结标题。XLS 保留已保存的值、公式、数字格式、合并及可用的行列元数据;不支持字体、边框和冻结窗格。工作簿显示已保存的公式结果而不重新计算;缺少的结果保持为空,公式栏旁的紧凑提示会标记显示结果可能不完整或不准确的工作簿。表格预览不调用 Office 转换服务。 +直接在浏览器中打开 `.xlsx`、`.xls`、`.csv` 和 `.tsv`,支持工作表标签、单元格选择、复制和只读公式栏。XLSX 保留字体、纯色填充、边框、数字格式、富文本、合并单元格、行列尺寸、隐藏行列及工作表,以及冻结标题。XLS 保留已保存的值、公式、数字格式、合并及可用的行列元数据;不支持字体、边框和冻结窗格。工作簿显示已保存的公式结果而不重新计算;缺少的结果保持为空,公式栏旁的紧凑提示会标记显示结果可能不完整或不准确的工作簿。工作簿视图随预览面板尺寸变化自动调整,无需重新加载工作表或选择状态。表格预览不调用 Office 转换服务。 XLSX 预览在解析前从内存副本中移除 DrawingML 部件,并忽略工作表的绘图引用。原文件和工作表 XML 保持不变。表格上方的提示列出实际检测到但未展示的图表、图片、形状和条件格式,并建议使用系统应用打开工作簿。未检测到这些内容的文件不显示此提示;公式提示保持独立。 diff --git a/packages/client/ui-sidebar-documentpreview/src/client/excel/excel.tsx b/packages/client/ui-sidebar-documentpreview/src/client/excel/excel.tsx index b8bbb89159..0e1b67da4b 100644 --- a/packages/client/ui-sidebar-documentpreview/src/client/excel/excel.tsx +++ b/packages/client/ui-sidebar-documentpreview/src/client/excel/excel.tsx @@ -1,5 +1,5 @@ /** Read-only spreadsheet surface backed by browser-parsed workbook data. */ -import { useEffect, useState, type ReactNode } from 'react' +import { useEffect, useRef, useState, type ReactNode } from 'react' import { Workbook } from '@fortune-sheet/react' import { Button, IconWarningTriangleOutlineRegular, StateDot, Tooltip } from '@deepseek-ai/dsh-client-ui-primitives' import fortuneCss from '@fortune-sheet/react/dist/index.css?inline' @@ -14,6 +14,7 @@ const scopedStyles = `@scope ([data-excel-preview]) { ${fortuneCss} }` /** * Display stored spreadsheet values and formats without editing or recalculation. + * Keep the mounted workbook sized to its preview pane. * @param props - Complete workbook bytes, limits, and locale. * @returns An isolated spreadsheet surface with cancellable loading. */ @@ -21,6 +22,7 @@ export function ExcelBody({ content, format, limits, t }: LoadedExcelBodyProps): const data = content.kind === 'bytes' ? content.data : undefined const [state, setState] = useState() const [attempt, setAttempt] = useState(0) + const workbookRef = useRef(null) useEffect(() => { if (data === undefined) return const controller = new AbortController() @@ -31,6 +33,14 @@ export function ExcelBody({ content, format, limits, t }: LoadedExcelBodyProps): ) return () => { controller.abort() } }, [data, format, limits, attempt]) + useEffect(() => { + const element = workbookRef.current + if (element === null || typeof ResizeObserver === 'undefined') return + // FortuneSheet measures its canvas on window resize, but pane resizing does not emit that event. + const observer = new ResizeObserver(() => { window.dispatchEvent(new Event('resize')) }) + observer.observe(element) + return () => { observer.disconnect() } + }, [state]) if (data === undefined) return

{t('invalid')}

if (state?.data !== data || state.format !== format) return @@ -46,7 +56,7 @@ export function ExcelBody({ content, format, limits, t }: LoadedExcelBodyProps): {t('unsupportedNotice', { features: state.value.unsupportedFeatures.map(feature => t(feature)).join(t('featureSeparator')) })} } -
+
{hasFormulas &&