Repository navigation
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
3 Skipped Deployments
|
Merging this PR will improve performance by 10.38%
|
| Benchmark | BASE |
HEAD |
Efficiency | |
|---|---|---|---|---|
| ⚡ | repeated[1000] |
2.4 ms | 2.2 ms | +10.38% |
Tip
Curious why performance improved? Comment @codspeedbot explain why performance improved on this PR, or directly use the CodSpeed MCP with your agent.
Comparing reza/fe-376-docx-file-previewing-in-hash (103d35f) with main (ece3dd1)1
Footnotes
820ac5e to
3629eb7
Compare
3629eb7 to
5442758
Compare
PR SummaryMedium Risk Overview Integration and build: Refactors: PDF search UI moves to shared Tests: Playwright Reviewed by Cursor Bugbot for commit 103d35f. Bugbot is set up for automated code reviews on this repo. Configure here. |
|
The Lines 31 to 34 in 2314834 stella/folio#1679 fixes this upstream by adding |
|
Follow-up to #9995 (comment): 20b203c moves the workaround out of the root
When we bump |
The File Preview section renders .docx files with @stll/folio-react in read-only mode, with page buttons, zoom, full screen and search, loaded only when a DOCX file is opened. The PDF and DOCX previews share the presigned-URL fetch. folio needs newer ProseMirror packages than HASH's editor, so a webpack rule resolves folio's tree to one copy of each. `.yarnrc.yml` adds folio-core's missing `y-protocols` dependency. The file-preview Playwright spec gains DOCX cases: paging, zoom and search; read-only; copy; a hostile document; `.doc` and corrupt files.
Move `pdf-preview/pdf-search.tsx` to `file-preview-search.tsx`, next to the PDF and DOCX previews, so the DOCX preview can use the same bar.
Move the PDF text extraction and hit finding into `pdf-preview/pdf-search-hits.ts`. `FilePreviewSearch` keeps the bar and its input state, and takes the hit count, the selected hit's index and callbacks to search and to select a hit, so it does not depend on pdf.js.
The DOCX preview now searches with `FilePreviewSearch`, like the PDF preview, instead of opening folio's own find dialog. `FolioDocument` finds the hits in folio's block snapshot and shows each one with `showInDocument`, which selects the exact range and scrolls to it. `keyboardShortcuts="none"` keeps folio's dialog closed. `@stll/folio-core` becomes a direct dependency, at the version `@stll/folio-react` uses, because the frontend now relies on its `ai-edits` types.
folio brings prosemirror-state 1.4.4 and prosemirror-transform 1.12.2. `yarn dedupe --strategy highest` then moves the `^1.0.0` ranges that HASH's prosemirror-view, -keymap and -commands use onto them, while HASH's own code stays on prosemirror-state 1.4.3. Each of those packages would get its own nested copy, and HASH's editor would mix copies. Pin those ranges to the versions they resolve to today, as the root `resolutions` already do for prosemirror-model and prosemirror-view, and dedupe the rest (reselect, valibot, @floating-ui/react-dom). The webpack rule that keeps folio on its own ProseMirror copies now fails the build if one of those copies is the frontend's own, because aliasing its imports would change the frontend's editor too.
The sidebar's entity list can still be loading when the check runs, and its spinners are not the preview's.
The DOCX tests that share a three-page document now upload it in a `beforeEach`, as the PDF tests do, so paging, zoom, search, read-only and copy each fail on their own.
folio-core moves to 0.58.5, the version folio-react 0.26.0 uses. Both, and `@stll/docx-core` 0.36.2, are younger than the 7-day age gate, so `npmPreapprovedPackages` admits exactly these versions. folio's commits up to 0.26.0 were read through before the update.
The `@stll/folio-react/messages` declaration re-exports folio-core's own types instead of a hand-written signature, so it returns `FolioMessages` and follows folio when it changes.
`showHit` marks a hit by selecting it, and a later search with no hits left that selection on screen. The search now collapses it.
An error before folio lays out any page, as with a corrupt file or one whose bookmarks folio rejects, now falls back to the icon view other files get. An error after pages are showing is reported to Sentry and leaves them in place. Download stays available either way.
…heck The DOCX preview's engine logs a `ProseMirrorDocumentValidationError` when it refuses a document, and the preview then shows the file's icon. The test that opens such a document on purpose failed on that console message.
The generator wrote the ZIP headers byte by byte, which a reviewer could only check against the ZIP specification. jszip is already in the lockfile through folio, so the generator now hands it the DOCX parts instead. The parts are unchanged, and the generators return a promise. The page size and margins move into a named constant with a comment on their units.
The shared list of tolerable console messages let every e2e test log a `ProseMirrorDocumentValidationError`. The console check now also reads a `tolerableConsoleMessages` option, and the test that opens a DOCX folio cannot draw sets it with `test.use`. Every other test still fails on that message.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit fbe9297. Configure here.
| const isDocx = | ||
| (mimeType === docxMimeType || | ||
| !!fileName?.toLowerCase().endsWith(".docx")) && | ||
| unavailableDocxUrl !== fileUrl; |
There was a problem hiding this comment.
Failed preview never retries same URL
Medium Severity
unavailableDocxUrl is never cleared, and handleError treats download failures like a corrupt file. A transient fetch error, or a later replacement that keeps the same fileUrl, leaves the icon view in place for the rest of the session.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit fbe9297. Configure here.
Move `pdf-preview/pdf-search-hits.ts` back to the path the file had before the search bar moved out, so the diff shows the file's changes rather than a deletion and a new file.
The rule now lives in `folio-single-prosemirror-rule.js`, and `next.config.js` imports it. The rule lists the packages it redirects in two arrays: those HASH's editor also installs, and those only folio uses. It no longer derives them from folio's `package.json` files with a regex. Node's `findPackageJSON` replaces the hand-written walk up the `node_modules` folders. The check that stops the build when folio and the frontend share a copy now runs over the first array, so it also covers `prosemirror-dropcursor`, which HASH installs through `hash-isomorphic-utils`. The rule's output is unchanged: the same 13 aliases and the same `include` directories.
Benchmark results
|
| Function | Value | Mean | Flame graphs |
|---|---|---|---|
| resolve_policies_for_actor | user: empty, selectivity: high, policies: 2002 | Flame Graph | |
| resolve_policies_for_actor | user: empty, selectivity: low, policies: 1 | Flame Graph | |
| resolve_policies_for_actor | user: empty, selectivity: medium, policies: 1002 | Flame Graph | |
| resolve_policies_for_actor | user: seeded, selectivity: high, policies: 3314 | Flame Graph | |
| resolve_policies_for_actor | user: seeded, selectivity: low, policies: 1 | Flame Graph | |
| resolve_policies_for_actor | user: seeded, selectivity: medium, policies: 1527 | Flame Graph | |
| resolve_policies_for_actor | user: system, selectivity: high, policies: 2078 | Flame Graph | |
| resolve_policies_for_actor | user: system, selectivity: low, policies: 1 | Flame Graph | |
| resolve_policies_for_actor | user: system, selectivity: medium, policies: 1033 | Flame Graph |
policy_resolution_medium
| Function | Value | Mean | Flame graphs |
|---|---|---|---|
| resolve_policies_for_actor | user: empty, selectivity: high, policies: 102 | Flame Graph | |
| resolve_policies_for_actor | user: empty, selectivity: low, policies: 1 | Flame Graph | |
| resolve_policies_for_actor | user: empty, selectivity: medium, policies: 52 | Flame Graph | |
| resolve_policies_for_actor | user: seeded, selectivity: high, policies: 269 | Flame Graph | |
| resolve_policies_for_actor | user: seeded, selectivity: low, policies: 1 | Flame Graph | |
| resolve_policies_for_actor | user: seeded, selectivity: medium, policies: 108 | Flame Graph | |
| resolve_policies_for_actor | user: system, selectivity: high, policies: 133 | Flame Graph | |
| resolve_policies_for_actor | user: system, selectivity: low, policies: 1 | Flame Graph | |
| resolve_policies_for_actor | user: system, selectivity: medium, policies: 63 | Flame Graph |
policy_resolution_none
| Function | Value | Mean | Flame graphs |
|---|---|---|---|
| resolve_policies_for_actor | user: empty, selectivity: high, policies: 2 | Flame Graph | |
| resolve_policies_for_actor | user: empty, selectivity: low, policies: 1 | Flame Graph | |
| resolve_policies_for_actor | user: empty, selectivity: medium, policies: 2 | Flame Graph | |
| resolve_policies_for_actor | user: system, selectivity: high, policies: 8 | Flame Graph | |
| resolve_policies_for_actor | user: system, selectivity: low, policies: 1 | Flame Graph | |
| resolve_policies_for_actor | user: system, selectivity: medium, policies: 3 | Flame Graph |
policy_resolution_small
| Function | Value | Mean | Flame graphs |
|---|---|---|---|
| resolve_policies_for_actor | user: empty, selectivity: high, policies: 52 | Flame Graph | |
| resolve_policies_for_actor | user: empty, selectivity: low, policies: 1 | Flame Graph | |
| resolve_policies_for_actor | user: empty, selectivity: medium, policies: 26 | Flame Graph | |
| resolve_policies_for_actor | user: seeded, selectivity: high, policies: 94 | Flame Graph | |
| resolve_policies_for_actor | user: seeded, selectivity: low, policies: 1 | Flame Graph | |
| resolve_policies_for_actor | user: seeded, selectivity: medium, policies: 27 | Flame Graph | |
| resolve_policies_for_actor | user: system, selectivity: high, policies: 66 | Flame Graph | |
| resolve_policies_for_actor | user: system, selectivity: low, policies: 1 | Flame Graph | |
| resolve_policies_for_actor | user: system, selectivity: medium, policies: 29 | Flame Graph |
read_scaling_complete
| Function | Value | Mean | Flame graphs |
|---|---|---|---|
| entity_by_id;one_depth | 1 entities | Flame Graph | |
| entity_by_id;one_depth | 10 entities | Flame Graph | |
| entity_by_id;one_depth | 25 entities | Flame Graph | |
| entity_by_id;one_depth | 5 entities | Flame Graph | |
| entity_by_id;one_depth | 50 entities | Flame Graph | |
| entity_by_id;two_depth | 1 entities | Flame Graph | |
| entity_by_id;two_depth | 10 entities | Flame Graph | |
| entity_by_id;two_depth | 25 entities | Flame Graph | |
| entity_by_id;two_depth | 5 entities | Flame Graph | |
| entity_by_id;two_depth | 50 entities | Flame Graph | |
| entity_by_id;zero_depth | 1 entities | Flame Graph | |
| entity_by_id;zero_depth | 10 entities | Flame Graph | |
| entity_by_id;zero_depth | 25 entities | Flame Graph | |
| entity_by_id;zero_depth | 5 entities | Flame Graph | |
| entity_by_id;zero_depth | 50 entities | Flame Graph |
read_scaling_linkless
| Function | Value | Mean | Flame graphs |
|---|---|---|---|
| entity_by_id | 1 entities | Flame Graph | |
| entity_by_id | 10 entities | Flame Graph | |
| entity_by_id | 100 entities | Flame Graph | |
| entity_by_id | 1000 entities | Flame Graph | |
| entity_by_id | 10000 entities | Flame Graph |
representative_read_entity
| Function | Value | Mean | Flame graphs |
|---|---|---|---|
| entity_by_id | entity type ID: https://blockprotocol.org/@alice/types/entity-type/block/v/1
|
Flame Graph | |
| entity_by_id | entity type ID: https://blockprotocol.org/@alice/types/entity-type/book/v/1
|
Flame Graph | |
| entity_by_id | entity type ID: https://blockprotocol.org/@alice/types/entity-type/building/v/1
|
Flame Graph | |
| entity_by_id | entity type ID: https://blockprotocol.org/@alice/types/entity-type/organization/v/1
|
Flame Graph | |
| entity_by_id | entity type ID: https://blockprotocol.org/@alice/types/entity-type/page/v/2
|
Flame Graph | |
| entity_by_id | entity type ID: https://blockprotocol.org/@alice/types/entity-type/person/v/1
|
Flame Graph | |
| entity_by_id | entity type ID: https://blockprotocol.org/@alice/types/entity-type/playlist/v/1
|
Flame Graph | |
| entity_by_id | entity type ID: https://blockprotocol.org/@alice/types/entity-type/song/v/1
|
Flame Graph | |
| entity_by_id | entity type ID: https://blockprotocol.org/@alice/types/entity-type/uk-address/v/1
|
Flame Graph |
representative_read_entity_type
| Function | Value | Mean | Flame graphs |
|---|---|---|---|
| get_entity_type_by_id | Account ID: bf5a9ef5-dc3b-43cf-a291-6210c0321eba
|
Flame Graph |
representative_read_multiple_entities
| Function | Value | Mean | Flame graphs |
|---|---|---|---|
| entity_by_property | traversal_paths=0 | 0 | |
| entity_by_property | traversal_paths=255 | 1,resolve_depths=inherit:1;values:255;properties:255;links:127;link_dests:126;type:true | |
| entity_by_property | traversal_paths=2 | 1,resolve_depths=inherit:0;values:0;properties:0;links:0;link_dests:0;type:false | |
| entity_by_property | traversal_paths=2 | 1,resolve_depths=inherit:0;values:0;properties:0;links:1;link_dests:0;type:true | |
| entity_by_property | traversal_paths=2 | 1,resolve_depths=inherit:0;values:0;properties:2;links:1;link_dests:0;type:true | |
| entity_by_property | traversal_paths=2 | 1,resolve_depths=inherit:0;values:2;properties:2;links:1;link_dests:0;type:true | |
| link_by_source_by_property | traversal_paths=0 | 0 | |
| link_by_source_by_property | traversal_paths=255 | 1,resolve_depths=inherit:1;values:255;properties:255;links:127;link_dests:126;type:true | |
| link_by_source_by_property | traversal_paths=2 | 1,resolve_depths=inherit:0;values:0;properties:0;links:0;link_dests:0;type:false | |
| link_by_source_by_property | traversal_paths=2 | 1,resolve_depths=inherit:0;values:0;properties:0;links:1;link_dests:0;type:true | |
| link_by_source_by_property | traversal_paths=2 | 1,resolve_depths=inherit:0;values:0;properties:2;links:1;link_dests:0;type:true | |
| link_by_source_by_property | traversal_paths=2 | 1,resolve_depths=inherit:0;values:2;properties:2;links:1;link_dests:0;type:true |
scenarios
| Function | Value | Mean | Flame graphs |
|---|---|---|---|
| full_test | query-limited | Flame Graph | |
| full_test | query-unlimited | Flame Graph | |
| linked_queries | query-limited | Flame Graph | |
| linked_queries | query-unlimited | Flame Graph |


🌟 Purpose
The entity editor's File Preview now shows uploaded
.docxfiles as pages, with the PDF preview's paging, zoom, full screen, search, and select-and-copy. Until now a.docxshowed only a file icon. The preview draws the file in the browser, so the file stays in HASH. Closes FE-376.The renderer is
@stll/folio-react(folio below), used read-only. It was chosen by an evaluation of browser DOCX libraries. Each candidate had to pass fixed criteria (runs in the browser, licence, download size, speed, no outbound requests), and those that passed were scored on layout fidelity, backing, integration effort and speed. Three passed: eigenpal docx-editor, folio and@silurus/ooxml. folio was chosen because readers can select and copy text, which eigenpal's view mode blocks, and because it stays usable with the JavaScript JIT blocked. The preview was tested in Chrome, Chrome 122, Edge, and Edge with the JIT blocked. With the JIT blocked, the first page of a real document takes up to 2.6 s, against about 1.2 s in Chrome.🔍 Changes
docx-preview/folio-document.tsxis the only file that imports folio. The rest of the preview uses it throughDocxDocumentControls.next.config.jsresolves every ProseMirror and yjs import inside folio's packages to folio's own copies. The build fails if folio would share a copy with HASH's editor.resolutionspin the^1.0.0and>=1.0.0ranges ofprosemirror-stateto 1.4.3. Like the existingprosemirror-modelandprosemirror-viewresolutions, they keep the block editor on one copy. HASH pinsprosemirror-stateat 1.4.3, and without the resolutionsyarn dedupe --strategy highestmoves the ranges that HASH's other ProseMirror packages use onto folio's 1.4.4..yarnrc.ymladdsy-protocolsto@stll/folio-core, which imports it throughy-prosemirrorwithout declaring it. Two.d.tsfiles make folio'smessagesandai-editssubpaths visible to the frontend'smoduleResolution: "node", and@stll/folio-coreis a direct dependency because they import it.@stll/folio-react0.26.0,@stll/folio-core0.58.5 and@stll/docx-core0.36.2 are younger than the 7-day minimum package age (npmMinimalAgeGate), sonpmPreapprovedPackagesin.yarnrc.ymladmits exactly these versions. I read through folio's commits from 2026-09-27 up to 0.26.0 first. They are signed and come from its one maintainer and its release bot. The npm packages are built from the tagged commits, and they add no install scripts, network calls or known advisories.FilePreviewSearch, shared by both previews, and both previews fetch the file withfetchPresignedFileUrl. The PDF preview behaves as before.file-preview.spec.tsgains DOCX tests next to the PDF tests from #9972, one per behaviour:javascript:link, adata:link, a remote picture and an HTML altChunk holding a script renders without scripts, iframes,javascript:ordata:links, or event-handler attributes. Clicking its links opens no dialog, and the page requests nothing outside localhost..doc: a.docfile keeps the icon view..docx: a corrupt file and a file with a bookmark folio rejects both show the icon view and keep Download.generate-test-docx.tswrites the documents in the test, so the PR adds no binary file..docfiles keep the icon view.🚫 Blocked by