fix(tiptap): validate node overlay message origin - #670
ornsteinfilip wants to merge 100 commits into
Conversation
…warning The "page is being edited" warning (ConsoleUrlBarComponent) produced false positives: console_url was only cleared on sign out, so closing the tab kept the lock alive for up to 5 minutes. The server-rendered bar also never went away without a full page reload, even after the other user left. - clear console_url via navigator.sendBeacon on pagehide (new console_url_clear API endpoint); the clear is conditional server-side (only when the stored console_url still matches the leaving page) so regular console navigation is unaffected; pageshow from bfcache re-pings to restore the lock - console_url_ping now responds with other_user_at_url instead of 204; the bar's Stimulus controller removes the plain presence variant once the other user is gone (revision-based variants are unaffected) - expose the bar variant (other_user / takeover / outdated) as a Stimulus value
The atom-editing overlay (position: fixed) and its dismiss element were nested inside .f-c-simple-form-with-atoms__scroll, which uses overflow: hidden in the horizontal layout. Browsers that clip fixed-position descendants of overflow-hidden ancestors (observed in the wild on macOS Safari) cut the overlay to the scroll box - its header with the Done/close buttons ended up hidden under the layout bars above (e.g. the "page is being edited" warning) and the open atom could not be saved or closed. Render the overlay as a direct child of the form root instead, so no ancestor can ever clip it. Sass and JS use flat BEM selectors and a delegated dismiss handler, so no other changes are needed.
* feat(tiptap): add nested node DSL * feat(tiptap): recurse through nested node content * test(tiptap): add dummy nested node example * feat(tiptap): add nested node overlay fields * docs(skills): clarify ruby keyword alignment * docs(tiptap): document nested nodes * style(tiptap): align nested node keywords * feat(nested_fields): support virtual collections * refactor(tiptap): use virtual nested fields * feat(tiptap): prebuild nested rows Default nested node fields to one blank row when empty and require at least one nested item on save. * docs(tiptap): update nested node docs Document nested node prebuild, params shape, sanitization, extraction, and placement behavior. * chore(i18n): update dummy card_group/card i18n * feat(simple_form): add character-counter class to form group * fix(character-counter): avoid duplicate nested counters * feat(tiptap): tweak nested nodes css and ux * docs(changelog): mention tiptap nested nodes * fix(tiptap): persist url_json record_id as integer * fix(tiptap): improve "" file_id handling * feat(tiptap): add disable_label support to url_json * fix(url_input): render hints after URL controls Keep url_json custom HTML before SimpleForm hints and align dynamic URL insertion with the same order. * docs(changelog): note tiptap and URL input fixes * feat(tiptap): add form layout support * docs(ai/skills): prefer kwargs over 3+ positional args in ruby * feat(nested_fields): add per-row add more control Add an opt-in add_more control that inserts a new nested field after the current row. Allow control tooltips to be enabled per NestedFieldsComponent usage and turn them on for Tiptap nested nodes. * fix(nested_fields): skip file picker inputs on focus * refactor(tiptap): drop nested node fields header * fix(tiptap): preserve nested validation labels * fix(nested_fields): close tooltip before duplicating * feat(nested_fields): tweak onDuplicateClick * docs(skills): add rails models validation guidance * fix(files): show picker validation state * feat(console): improve input URL invalid state
* feat(tiptap): add color node attributes Normalize supported CSS color formats to persisted hex values and render color inputs in the Tiptap overlay. * chore(tiptap): add color to dummy tiptap node card structure * chore(changelog): note color field
by_query relied solely on pg_search full-text search. PostgreSQL stores dotted filenames such as "name.com_123456.mp4" as a single `host` lexeme, while pg_search splits the query on dots and ANDs the terms, so the "com" term never matched and searching the whole filename returned nothing. Add a raw file_name ILIKE substring fallback (mirroring the existing slug match) so files are found by their filename regardless of FTS tokenization.
… + nested routes (#649) * fix(console): keep editing-presence heartbeat alive for a lone editor The 10s presence heartbeat lived inside the warning bar, which only renders once another editor (or a conflicting revision) is detected. So an editor alone on a page never pinged and silently expired from the 5-minute presence window, becoming invisible to anyone who opened the same record later and letting two editors overwrite each other. - Extract the heartbeat into a dedicated PresencePingComponent rendered on every console edit/update independently of the warning bar; re-ping on tab visibilitychange. The bar reacts to a folio:console:presence-ping window event instead of pinging itself. - Track presence under a canonical record URL (the edit URL) consistently across the server write, heartbeat, warning-bar lookup and pagehide/pageshow beacon, so the edit page and a form re-rendered after a failed update are no longer treated as different URLs. - console_url_ping renders a warning bar (bar_html) when another editor appears and the heartbeat injects it live, warning the first editor without a reload; the rendered record is bound to the pinged URL and authorized. * fix(console): sign presence placement instead of re-deriving its URL The live warning bar was bound to the pinged URL by regenerating the record's edit path server-side (polymorphic_path), which cannot produce the URL for nested console routes that need a parent id the ping request does not carry — so a lone editor of a nested resource never received the live bar. Sign { type, id, url } into a placement token when rendering the page (PresencePingComponent) and verify it in console_url_ping instead of re-deriving the URL. Works for any route, keeps the URL/record binding tamper-proof, and still authorizes the record before rendering. * chore(console): drop internal tracker refs from presence comments/tests * fix(console): derive presence URL via safe_url_for to avoid 500 on non-edit routes folio_console_presence_url generated the canonical edit URL with a bare url_for(action: :edit, id:), which raises ActionController::UrlGenerationError for a nested route whose parent id is not in request scope, or for a resource that has no edit route — 500-ing every edit/update of such a resource. Derive it via safe_url_for and fall back to request.url when generation fails, so presence degrades gracefully instead of raising.
Show short-query guidance for the React ordered multiselect autocomplete while preserving blank-query option loading.
# Conflicts: # CHANGELOG.md
Add virtual ordered multiselect data plumbing, array serialization, react_select label_method support, focused tests, and rebuilt React assets.
…erence Add a `default_responsive_preview` input value sourced from the current user's `mobile_first` console preference. It flows through the Stimulus controller and the `f-input-tiptap:start` message into the React editor's initial responsive-preview state. The toolbar toggle still switches back and the manual choice is not persisted. Only the block editor is affected, since rich-text fields have no responsive toggle.
…eview feat(tiptap): default responsive preview from user console preference
The console loads records via FriendlyId, which matches a numeric :id param against the slug column before the primary key. A record whose slug equals another record's id (e.g. a file uploaded as "349444.jpg" gets slug "349444", colliding with id 349444) hijacked the lookup, so opening/selecting/editing that id returned the wrong record. - Console finder prefers the primary key for numeric params, falling back to FriendlyId only for genuine (non-numeric) slugs. - Folio::File slugs can no longer be purely numeric, so the slug and id namespaces never overlap (covers public download/video lookups too).
Allow a Tiptap node to replace its overlay field layout with one ViewComponent.
This reverts commit 9e948d2.
…e row" This reverts commit 4b22e18.
This reverts commit cf8833d.
…ax restriction Tempus Dominus seeds viewDate to the current moment (incl. time-of-day) when the input has no value. Clicking a day then compares that leftover time-of-day against a midnight-based min/maxDate, so e.g. today could fail to be selectable until another date was picked first. Seed viewDate at midnight for empty date-only inputs, and format min/max without a spurious time component to match.
1c8281a to
f519053
Compare
mreq
left a comment
There was a problem hiding this comment.
Nerozumim, co presne to opravuje. Kdy jsi narazil na to, ze se window.origin a window.location.origin lisi? Protoze pokud jsou stejne, tak ten kod nic nemeni.
Short answer: on a normal HTTPS page, they usually return the same string. But they are different APIs:
window.originis the effective origin of the global browsing context.window.location.originis derived from the page URL.
They can differ in sandboxed or inherited-origin frames, and location.origin has broader established browser support. MDN documents the distinction.
Findings
1. [WARNING] The fix is only partial
The PR functionally changes:
event.origin !== window.originto comparison with:
window.location.originThe window.origin you saw at the end of the generated bundle belongs to a separate listener in main.tsx.
However, other listeners—including the existing-node handler for the same f-c-tiptap-overlay:saved message—still use window.origin. If window.origin caused the production failure, those paths remain vulnerable to the same problem.
2. [SUGGESTION] Most added code is unnecessary scaffolding
The minimal fix is simply:
event.origin !== window.location.originThe new helper only performs string equality. Its tests always inject both strings, so they don’t exercise the default window.location.origin or the actual message handler.
Overall Assessment
Your instinct is basically right: there is a small real behavioral change—window.origin becomes window.location.origin—but it is wrapped in considerably more code than needed. More importantly, the PR doesn’t consistently apply that change to the other message listeners, so the claimed root cause and the scope of the fix don’t fully line up.
Context Used
Reviewed PR #670 against its declared base branch, plus AGENTS.md, tiptap/AGENTS.md, .skills/code-review, .skills/folio-javascript, and .skills/folio-testing.
The add-embed button queried the first .f-nested-fields in the whole form, so it silently added the embed row to an unrelated nested-fields collection whenever the form rendered another folio_nested_fields before the picker — the button appeared to do nothing. Resolve the multi picker first and take its nested fields. The lookup cannot rely on closest() alone: the source header holding the button is detached into .f-c-tiptap-simple-form-wrap, so it is no longer a descendant of its own picker. Fall back to the picker within the form, the same way simple_form_wrap_component resolves it for addToPicker.
Iframes pasted as raw HTML (e.g. a copied YouTube <iframe> embed snippet) kept their literal width/height attributes with no responsive sizing, so they overflowed and got clipped by the embed box's overflow:hidden in narrower containers. Derive an aspect-ratio from the iframe's width/height attributes and scale it to the container width instead.
The YouTube URL embed path renders its iframe with literal width/height attributes (560x315, or 360x640 for Shorts). The .f-embed__youtube-iframe rule only set max-width, so the iframe could shrink in narrow containers but never grew past its attribute width in wider ones. Set width: 100% so the existing aspect-ratio scales it to the container in both directions.
Embedded iframes are sized by their width/height attributes, which made a raw-HTML embed overflow and get clipped in a narrower container. Derive an aspect ratio from those attributes and cap them with max-width so they shrink to fit, which applies everywhere. Growing past the attribute width is a design decision, not a bug fix, so it stays opt-in: full_width_iframes: true forwards fullWidthIframes=1 to /folio/embed, which adds f-embed__container--full-width-iframes and applies width: 100%. Host apps rendering embeds on several sites can enable it per site. The option is also accepted by `input as: :embed` and by :embed tiptap node attributes, where a Proc is resolved at render time, so console previews match the frontend.
Iframes pasted as raw HTML (e.g. a copied YouTube <iframe> embed snippet) kept their literal width/height attributes with no responsive sizing, so they overflowed and got clipped by the embed box's overflow:hidden in narrower containers. Derive an aspect-ratio from the iframe's width/height attributes and scale it to the container width instead.
The YouTube URL embed path renders its iframe with literal width/height attributes (560x315, or 360x640 for Shorts). The .f-embed__youtube-iframe rule only set max-width, so the iframe could shrink in narrow containers but never grew past its attribute width in wider ones. Set width: 100% so the existing aspect-ratio scales it to the container in both directions.
Embedded iframes are sized by their width/height attributes, which made a raw-HTML embed overflow and get clipped in a narrower container. Derive an aspect ratio from those attributes and cap them with max-width so they shrink to fit, which applies everywhere. Growing past the attribute width is a design decision, not a bug fix, so it stays opt-in: full_width_iframes: true forwards fullWidthIframes=1 to /folio/embed, which adds f-embed__container--full-width-iframes and applies width: 100%. Host apps rendering embeds on several sites can enable it per site. The option is also accepted by `input as: :embed` and by :embed tiptap node attributes, where a Proc is resolved at render time, so console previews match the frontend.
…beds fix(embed): responsive raw-HTML/YouTube iframes + opt-in full width
Feat/virtual ordered multiselect
* feat(audio): add private audio processing pipeline * fix(audio): address code review — cleanup, correctness, test fixes - remove unused user_id param from extract_metadata! - remove always-true is_a?(Folio::File) guard in should_extract_metadata? - playable_download_url: remove storage guard, delegate to test_aware_presign_url - store_derivative: unify to test_aware_s3_upload, delete old S3 derivative before re-upload - persist!: replace write_attribute loop with assign_attributes - duration_seconds: ceil → round - create_or_update_artwork_image: log warning on artwork extraction failure - file_serializer: extract source_mime_type_for private class method - tests: replace Mocha stubs with Minitest block-based stub, fix duration assertion, add non-private file serializer test * feat(audio): add audio duration formatting and display it in detail and list * feat(audio): implement AudioFieldMapper to extract and sync audio metadata to database fields * fix(audio): use playable MIME type in public serializer * fix(audio): fall back to original source before processing * fix(audio): retry failed processing before rerun * fix(audio): keep old playable until replacement persists * fix(audio): skip audio in media recovery job * fix(audio): memoize missing artwork image * fix(audio): respect playable URL expiration * refactor(audio): simplify metadata field mapping * refactor(audio): remove unused metadata helpers * fix(audio): block public downloads for private files * fix(audio): keep processed files ready when broadcast fails * revert(audio): keep public download redirects unchanged * fix(audio): decouple playback source from file privacy * fix(audio): split cacheable and immediate source payloads * test(audio): run only CreateFileJob in shared files upload test * fix(audio): accept x-aac and vnd.dlna.adts MIME types * fix(audio): accept x-aac and vnd.dlna.adts MIME types * feat(audio): add waveform generation and payload handling for audio files * feat(audio): console-managed player artwork for audio files * fix(audio): harden console-managed artwork handling * fix(audio): analyze extracted artwork so image processing completes * fix(audio): align displayed duration with player * fix(player): use vertical volume control on narrow audio players * fix(player): use vertical volume popup on audio players * revert(player): shelve vertical volume popup for now * fix(audio): make file_track_duration the canonical duration * fix(audio): stream waveform peak extraction * fix(audio): handle AAC MIME variants in direct copy * docs(changelog): document audio processing pipeline --------- Co-authored-by: Filip Ornstein <filip@sinfin.cz> Co-authored-by: Tomáš Cabák <tomascabak00@gmail.com>
f519053 to
525d1b5
Compare
525d1b5 to
9077933
Compare
Fixes a production-only TipTap node overlay round-trip failure.\n\nThe new-node listener now compares MessageEvent.origin with window.location.origin through a tested helper. Existing message flows remain unchanged.\n\nVerification:\n- npm test\n- npm run build:check\n- npm run build\n- ESLint and Prettier on touched files