-
-
Notifications
You must be signed in to change notification settings - Fork 547
feat(web): add fullscreen table preview and export actions #1693
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
techotaku39
wants to merge
21
commits into
tiann:main
Choose a base branch
from
techotaku39:feat/web-table-actions-fullscreen
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 12 commits
Commits
Show all changes
21 commits
Select commit
Hold shift + click to select a range
ab56255
feat(web): add fullscreen table preview and export actions
techotaku39 3c18098
fix(web): harden table exports and mobile detection
techotaku39 9b29c4d
fix(web): address table viewer review findings
techotaku39 c12f955
fix(web): exclude table controls from shared images
techotaku39 830e0fa
test(web): stabilize landscape table coverage
techotaku39 d9ea439
feat(web): complete table preview actions and export fidelity
techotaku39 13d5c04
fix(web): harden table export and preview coverage
techotaku39 eec6d66
fix(web): address table preview review findings
techotaku39 e693ea0
fix(web): harden table preview sizing
techotaku39 37cc1ba
fix(web): align table actions and export tiles
techotaku39 01b9d2c
fix(web): preserve table preview sizing
techotaku39 ee252a3
fix(web): report table image action failures
techotaku39 e3e1c0a
fix(web): handle pending mobile orientation locks
techotaku39 e1a961e
fix(web): preserve markdown table cell formatting
techotaku39 e2f3cea
fix(web): handle synchronous image clipboard errors
techotaku39 538c12c
fix(web): preserve rich markdown table links
techotaku39 06a9b3a
fix(web): preserve authored table link targets
techotaku39 bb6c311
fix(web): close pending table viewer work
techotaku39 638c0bd
fix(web): escape markdown table text safely
techotaku39 b21a912
fix(web): round-trip table media text safely
techotaku39 8dbd458
fix(web): preserve table header cell colors
techotaku39 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| <!doctype html> | ||
| <html lang="en"> | ||
| <head> | ||
| <meta charset="UTF-8" /> | ||
| <meta name="viewport" content="width=device-width, initial-scale=1" /> | ||
| <title>HAPI Markdown table fixture</title> | ||
| <style> | ||
| html { background: #fff; } | ||
| body { margin: 0; padding: 24px; font-family: system-ui, sans-serif; } | ||
| #root { max-width: 960px; margin: 0 auto; } | ||
| </style> | ||
| </head> | ||
| <body> | ||
| <div id="root"></div> | ||
| <script type="module" src="./markdown-table-fixture.tsx"></script> | ||
| </body> | ||
| </html> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,46 @@ | ||
| import React from 'react' | ||
| import ReactDOM from 'react-dom/client' | ||
| import '../src/index.css' | ||
| import { HappyChatProvider, type HappyChatContextValue } from '../src/components/AssistantChat/context' | ||
| import { I18nProvider } from '../src/lib/i18n-context' | ||
| import { MarkdownRenderer } from '../src/components/MarkdownRenderer' | ||
|
|
||
| const TABLE_MARKDOWN = `# Repository activity | ||
|
|
||
| | Project | Stars | Language | Latest release | Maintainer | Notes | | ||
| | --- | ---: | --- | --- | --- | --- | | ||
| | HAPI | 128 | TypeScript | 0.28.0 | Local-first team | Remote control for coding agents | | ||
| | HAPI, local-first | 42 | TypeScript | 0.27.3 | Community | A deliberately long description for horizontal table scrolling | | ||
| | Example | 7 | Rust | 1.2.0 | Open source | Stable fixture row |` | ||
|
|
||
| const NEAR_BOTTOM_VERTICAL_TABLE_MARKDOWN = `# Near bottom table | ||
|
|
||
| | Item | Status | | ||
| | --- | --- | | ||
| ${Array.from({ length: 14 }, (_, index) => `| Item ${index + 1} | Ready |`).join('\n')}` | ||
|
|
||
| function MarkdownTableFixture() { | ||
| const query = new URLSearchParams(window.location.search) | ||
| const content = query.has('near-bottom-scroll') | ||
| ? NEAR_BOTTOM_VERTICAL_TABLE_MARKDOWN | ||
| : TABLE_MARKDOWN | ||
|
|
||
| return ( | ||
| <HappyChatProvider value={{ sessionTitle: 'Table filename fixture' } as HappyChatContextValue}> | ||
| <main data-testid="markdown-table-fixture"> | ||
| <MarkdownRenderer standalone content={content} /> | ||
| </main> | ||
| </HappyChatProvider> | ||
| ) | ||
| } | ||
|
|
||
| const root = document.getElementById('root') | ||
| if (root) { | ||
| ReactDOM.createRoot(root).render( | ||
| <React.StrictMode> | ||
| <I18nProvider> | ||
| <MarkdownTableFixture /> | ||
| </I18nProvider> | ||
| </React.StrictMode>, | ||
| ) | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,124 @@ | ||
| import { devices, expect, test } from '@playwright/test' | ||
|
|
||
| test.use({ ...devices['Pixel 7'] }) | ||
|
|
||
| test('mobile markdown table viewer requests landscape and releases orientation controls', async ({ page }) => { | ||
| await page.goto('/e2e-fixtures/markdown-table-fixture.html') | ||
| await page.evaluate(() => { | ||
| const state = { requestFullscreen: 0, exitFullscreen: 0, locks: [] as string[], unlocks: 0 } | ||
| Object.defineProperty(window, '__hapiTableViewerState', { configurable: true, value: state }) | ||
| Object.defineProperty(document.documentElement, 'requestFullscreen', { | ||
| configurable: true, | ||
| value: () => { | ||
| state.requestFullscreen += 1 | ||
| return Promise.resolve() | ||
| }, | ||
| }) | ||
| Object.defineProperty(document, 'exitFullscreen', { | ||
| configurable: true, | ||
| value: () => { | ||
| state.exitFullscreen += 1 | ||
| return Promise.resolve() | ||
| }, | ||
| }) | ||
| Object.defineProperty(window.screen, 'orientation', { | ||
| configurable: true, | ||
| value: { | ||
| lock: (value: string) => { | ||
| state.locks.push(value) | ||
| return Promise.resolve() | ||
| }, | ||
| unlock: () => { | ||
| state.unlocks += 1 | ||
| }, | ||
| }, | ||
| }) | ||
| }) | ||
|
|
||
| const inlineActions = page.locator('[data-testid="markdown-table-fixture"] .aui-md-table-actions') | ||
| await expect(inlineActions).toBeVisible() | ||
| await expect(inlineActions.getByRole('button')).toHaveCount(1) | ||
| await page.getByRole('button', { name: 'Open table full screen' }).click() | ||
| const dialog = page.getByRole('dialog', { name: 'Table filename fixture' }) | ||
| await expect(dialog).toBeVisible() | ||
| // A real mobile browser can rotate to a landscape CSS viewport. Keep the | ||
| // mobile title unshifted even when its width becomes desktop-sized. | ||
| await page.setViewportSize({ width: 915, height: 412 }) | ||
| const wrapButton = dialog.locator('button[data-hapi-table-wrap-toggle="true"]') | ||
| await expect(wrapButton).toBeVisible() | ||
| await expect(wrapButton).toHaveAttribute('aria-pressed', /true|false/) | ||
| await expect(dialog.getByRole('button', { name: 'Copy table as Markdown' })).toBeVisible() | ||
| const downloadButton = dialog.getByRole('button', { name: 'Download table' }) | ||
| await expect(downloadButton).toBeVisible() | ||
| const initiallyWrapped = await wrapButton.getAttribute('aria-pressed') | ||
| await wrapButton.click() | ||
| await expect(wrapButton).toHaveAttribute('aria-pressed', initiallyWrapped === 'true' ? 'false' : 'true') | ||
| await expect.poll(() => dialog.locator('[data-hapi-table-viewer-toolbar="true"]').evaluate((element) => { | ||
| const style = getComputedStyle(element) | ||
| return `${style.paddingLeft}:${style.paddingRight}:${style.paddingTop}:${style.paddingBottom}` | ||
| })).toBe('6px:6px:0px:0px') | ||
| await expect.poll(() => dialog.locator('[data-hapi-table-viewer-toolbar="true"]').evaluate((element) => getComputedStyle(element).columnGap)).toBe('4px') | ||
| await expect.poll(() => dialog.locator('[data-hapi-table-viewer-heading="true"]').evaluate((element) => getComputedStyle(element).transform)).toBe('none') | ||
| await expect.poll(() => dialog.locator('[data-hapi-table-viewer="true"] thead th').first().evaluate((element) => { | ||
| const thead = element.closest('thead') | ||
| return `${getComputedStyle(thead ?? element).position}:${getComputedStyle(element).position}:${getComputedStyle(element).top}` | ||
| })).toBe('static:sticky:0px') | ||
| await expect.poll(() => page.evaluate(() => { | ||
| const state = (window as Window & { __hapiTableViewerState?: { requestFullscreen: number; locks: string[] } }).__hapiTableViewerState | ||
| return state ? `${state.requestFullscreen}:${state.locks.join(',')}` : '' | ||
| })).toBe('1:landscape') | ||
|
|
||
| await downloadButton.click() | ||
| await expect(page.getByRole('menuitem', { name: 'Download PNG' })).toBeVisible() | ||
| await expect(page.getByRole('menuitem', { name: 'Download CSV' })).toBeVisible() | ||
| const imageDownloadPromise = page.waitForEvent('download') | ||
| await page.getByRole('menuitem', { name: 'Download PNG' }).click() | ||
| const imageDownload = await imageDownloadPromise | ||
| expect(imageDownload.suggestedFilename()).toMatch(/^HAPI Table-Table filename fixture-\d{14}\.png$/) | ||
|
|
||
| const csvDownloadPromise = page.waitForEvent('download') | ||
| await downloadButton.click() | ||
| await page.getByRole('menuitem', { name: 'Download CSV' }).click() | ||
| const csvDownload = await csvDownloadPromise | ||
| expect(csvDownload.suggestedFilename()).toMatch(/^HAPI Table-Table filename fixture-\d{14}\.csv$/) | ||
|
|
||
| await dialog.getByRole('button', { name: 'Close table full screen' }).click() | ||
| await expect.poll(() => page.evaluate(() => { | ||
| const state = (window as Window & { __hapiTableViewerState?: { exitFullscreen: number; unlocks: number } }).__hapiTableViewerState | ||
| return state ? `${state.exitFullscreen}:${state.unlocks}` : '' | ||
| })).toBe('1:1') | ||
| }) | ||
|
|
||
| test('mobile markdown table viewer detects a phone that starts in landscape', async ({ page }) => { | ||
| await page.setViewportSize({ width: 915, height: 412 }) | ||
| await page.goto('/e2e-fixtures/markdown-table-fixture.html') | ||
| await expect(page.getByRole('button', { name: 'Open table full screen' })).toBeVisible() | ||
| await page.evaluate(() => { | ||
| const state = { requestFullscreen: 0, locks: [] as string[] } | ||
| Object.defineProperty(window, '__hapiTableViewerState', { configurable: true, value: state }) | ||
| Object.defineProperty(document.documentElement, 'requestFullscreen', { | ||
| configurable: true, | ||
| value: () => { | ||
| state.requestFullscreen += 1 | ||
| return Promise.resolve() | ||
| }, | ||
| }) | ||
| Object.defineProperty(window.screen, 'orientation', { | ||
| configurable: true, | ||
| value: { | ||
| lock: (value: string) => { | ||
| state.locks.push(value) | ||
| return Promise.resolve() | ||
| }, | ||
| unlock: () => {}, | ||
| }, | ||
| }) | ||
| }) | ||
|
|
||
| await page.getByRole('button', { name: 'Open table full screen' }).click() | ||
| await expect(page.getByRole('dialog', { name: 'Table filename fixture' })).toBeVisible() | ||
| await expect.poll(() => page.evaluate(() => { | ||
| const state = (window as Window & { __hapiTableViewerState?: { requestFullscreen: number; locks: string[] } }).__hapiTableViewerState | ||
| return state ? `${state.requestFullscreen}:${state.locks.join(',')}` : '' | ||
| })).toBe('1:landscape') | ||
| }) |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[MAJOR] Prebundle the remaining Workbox client before making this suite mandatory.
The clean current-head run fails here. Its log reports
new dependencies optimized: workbox-windowfollowed byoptimized dependencies changed. reloading; that reload tears down the mobile table dialog, and the close-button click times out on a detached element.web/vite.config.ts:183already prebundles Workbox modules specifically to prevent this E2E reload, but omitsworkbox-window.Suggested fix: