Skip to content

fix(tiptap): validate node overlay message origin - #670

Open
ornsteinfilip wants to merge 100 commits into
gaz-166-s3-multipart-uploadfrom
fix/tiptap-overlay-origin
Open

ornsteinfilip wants to merge 100 commits into
gaz-166-s3-multipart-uploadfrom
fix/tiptap-overlay-origin

Conversation

@ornsteinfilip

Copy link
Copy Markdown
Member

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

jirkamotejl and others added 30 commits June 5, 2026 13:45
…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).
mreq and others added 9 commits July 28, 2026 13:36
Allow a Tiptap node to replace its overlay field layout with one ViewComponent.
…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.
@ornsteinfilip
ornsteinfilip force-pushed the fix/tiptap-overlay-origin branch from 1c8281a to f519053 Compare July 30, 2026 06:11

@mreq mreq left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.origin is the effective origin of the global browsing context.
  • window.location.origin is 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.origin

to comparison with:

window.location.origin

The 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.origin

The 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.

mreq and others added 14 commits July 30, 2026 11:04
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(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>
@dedekm
dedekm force-pushed the fix/tiptap-overlay-origin branch from f519053 to 525d1b5 Compare August 29, 2026 10:32
@dedekm
dedekm force-pushed the fix/tiptap-overlay-origin branch from 525d1b5 to 9077933 Compare August 29, 2026 13:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

9 participants