From 011feeaf7175837167481b187f0ece924e8d5a07 Mon Sep 17 00:00:00 2001 From: Eva Marco Date: Wed, 9 Sep 2026 09:56:10 +0200 Subject: [PATCH] :bug: Fix missing text in legacy SVG board thumbnails (#11552) * :bug: Fix missing text in legacy SVG board thumbnails Board thumbnails rendered by frame-imposter used React's renderToStaticMarkup, a synchronous pass with no live DOM to measure text against. Text shapes without a persisted position-data value therefore rendered as nothing, so a cached board thumbnail silently lost its text until the board was hovered, selected, or the canvas was zoomed past 130%, all of which bypass the cached thumbnail in favor of live content. frame-imposter now provides the same is-render? context the standalone exporter already sets, so text without position-data falls back to the synchronous foreignObject renderer instead of rendering nothing. Thumbnails cached before this fix stay broken until something regenerates them, so on each page load, board thumbnails containing text are opportunistically regenerated once per browser (tracked via local-storage) so existing files self-heal without requiring an edit. AI-assisted-by: claude-sonnet-5 * :recycle: Use a transducer in heal-stale-text-thumbnails Fixes a PR review comment: the frame filtering/mapping was spread across four separate rx operators. Collapse it into a single transducer pass over frame-ids, leaving only one rx/map to perform the mark-healed side effect and build the update-thumbnail action. --- .../app/main/data/workspace/thumbnails.cljs | 55 ++++++++++++++++++- frontend/src/app/main/render.cljs | 21 +++---- 2 files changed, 65 insertions(+), 11 deletions(-) diff --git a/frontend/src/app/main/data/workspace/thumbnails.cljs b/frontend/src/app/main/data/workspace/thumbnails.cljs index 3526c1ed99..78392a82bd 100644 --- a/frontend/src/app/main/data/workspace/thumbnails.cljs +++ b/frontend/src/app/main/data/workspace/thumbnails.cljs @@ -12,6 +12,7 @@ [app.common.thumbnails :as thc] [app.common.time :as ct] [app.common.types.component :as ctc] + [app.common.types.shape-tree :as ctt] [app.common.uuid :as uuid] [app.main.data.changes :as dch] [app.main.data.helpers :as dsh] @@ -23,6 +24,7 @@ [app.main.render :as render] [app.main.repo :as rp] [app.util.queue :as q] + [app.util.storage :as storage] [app.util.timers :as tm] [app.util.webapi :as wapi] [beicon.v2.core :as rx] @@ -289,13 +291,60 @@ (mapcat get-frame-ids-cached)) changes)))) +;; Board thumbnails used to render text shapes without position-data as +;; nothing instead of falling back to the foreignObject renderer (see +;; frame-imposter in app.main.render), so any board thumbnail cached before +;; that fix may be missing its text. The backend doesn't tell the client +;; when a fetched thumbnail was generated, so we can't tell stale apart from +;; fresh by inspecting it; instead each board thumbnail with text content is +;; regenerated at most once per browser, tracked via local-storage so repeat +;; visits (once healed) don't keep re-rendering it. +(def ^:private healed-storage-key ::healed-text-thumbnails) + +(defn- frame-has-text? + [objects frame-id] + (->> (cfh/get-children-with-self objects frame-id) + (some cfh/text-shape?) + (some?))) + +(defn- unhealed-text-thumbnail? + [state object-id] + (and (some? (dm/get-in state [:thumbnails object-id :uri])) + (not (contains? (get @storage/global healed-storage-key) object-id)))) + +(defn- mark-thumbnail-healed! + [object-id] + (swap! storage/global update healed-storage-key (fnil conj #{}) object-id)) + +(defn- heal-stale-text-thumbnails + "Emits an `update-thumbnail` for every board on the page that has text + content and hasn't already been healed (see `healed-storage-key`) in this + browser." + [state file-id page-id] + (let [objects (-> (dsh/lookup-file-data state file-id) + (dsh/get-page page-id) + :objects) + frame-ids (ctt/get-root-frames-ids objects) + xf (comp + (filter #(frame-has-text? objects %)) + (keep + (fn [frame-id] + (let [object-id (thc/fmt-object-id file-id page-id frame-id "frame")] + (when (unhealed-text-thumbnail? state object-id) + [frame-id object-id])))))] + (->> (rx/from (eduction xf frame-ids)) + (rx/map + (fn [[frame-id object-id]] + (mark-thumbnail-healed! object-id) + (update-thumbnail file-id page-id frame-id "frame" "heal-stale-text-thumbnails")))))) + (defn watch-state-changes "Watch the state for changes inside frames. If a change is detected will force a rendering of the frame data so the thumbnail can be updated." [file-id page-id] (ptk/reify ::watch-state-changes ptk/WatchEvent - (watch [_ _ stream] + (watch [_ state stream] (let [stopper-s (rx/filter (fn [event] (as-> (ptk/type event) type @@ -330,6 +379,10 @@ (rx/tap #(l/trc :hint "buffer initialized")))] (->> (rx/merge + ;; Heal boards with text whose cached thumbnail may predate the + ;; text-position fix (see heal-stale-text-thumbnails). + (heal-stale-text-thumbnails state file-id page-id) + ;; Perform instant thumbnail cleaning of affected frames ;; and interrupt any ongoing update-thumbnail process ;; related to current frame-id diff --git a/frontend/src/app/main/render.cljs b/frontend/src/app/main/render.cljs index 31108fa9f0..36532ed591 100644 --- a/frontend/src/app/main/render.cljs +++ b/frontend/src/app/main/render.cljs @@ -266,16 +266,17 @@ [{:keys [objects frame vbox x y width height background]}] (let [shape-wrapper (shape-wrapper-factory objects)] [:& (mf/provider muc/render-thumbnails) {:value false} - [:svg {:view-box vbox - :width (ust/format-precision width viewbox-decimal-precision) - :height (ust/format-precision height viewbox-decimal-precision) - :version "1.1" - :xmlns "http://www.w3.org/2000/svg" - :xmlnsXlink "http://www.w3.org/1999/xlink" - :fill "none"} - (when (some? background) - [:rect {:x x :y y :width width :height height :fill background}]) - [:& shape-wrapper {:shape frame}]]])) + [:& (mf/provider muc/is-render?) {:value true} + [:svg {:view-box vbox + :width (ust/format-precision width viewbox-decimal-precision) + :height (ust/format-precision height viewbox-decimal-precision) + :version "1.1" + :xmlns "http://www.w3.org/2000/svg" + :xmlnsXlink "http://www.w3.org/1999/xlink" + :fill "none"} + (when (some? background) + [:rect {:x x :y y :width width :height height :fill background}]) + [:& shape-wrapper {:shape frame}]]]])) ;; Component that serves for render frame thumbnails, mainly used in ;; the viewer and inspector