From c1f48a9f0cda984368ff5ff01eca53a2fc0a7e3b Mon Sep 17 00:00:00 2001 From: Pablo Alba Date: Wed, 13 May 2026 22:02:00 +0200 Subject: [PATCH] :bug: Fix update library dialog when a component position changes Do not show the library sync popup when the only differences are global x/y changes on library components. We now generate the actual sync changes and only notify if there are real redo-changes to apply. Run cll/generate-sync-file-changes for candidate libraries and filter out those with empty :redo-changes. The expensive check is deferred via rx/timer 0 so it runs asynchronously and does not block the UI. Why: Position-only changes are normalized during sync (via reposition-shape) and never propagate to copies; showing the popup in that case was a false positive. Performance: The check is deferred to the next tick to avoid UI stutter on large files with many libraries. --- CHANGES.md | 1 + .../app/main/data/workspace/libraries.cljs | 71 ++++++++++++------- 2 files changed, 48 insertions(+), 24 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 05346225e9..d5cc61af20 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -26,6 +26,7 @@ - Fix lost-update race on `team.features` during concurrent file creation (by @web-dev0521) [Github #9197](https://github.com/penpot/penpot/issues/9197) - Fix copy and paste actions crashing the workspace on insecure origins (plain HTTP / non-`localhost`) where the Clipboard API is unavailable (by @MilosM348) [Github #6514](https://github.com/penpot/penpot/issues/6514) - Fix blend-mode dropdown leaving the canvas rendered with the last hover-preview blend mode when dismissed without selecting an option; the WASM render is now reverted to the saved blend mode on pointer-leave (by @edwin-rivera-dev) [Github #XXXX](https://github.com/penpot/penpot/issues/XXXX) +- Fix update library dialog when a component position changes [Taiga #11981](https://tree.taiga.io/project/penpot/issue/11981) ## 2.16.0 (Unreleased) diff --git a/frontend/src/app/main/data/workspace/libraries.cljs b/frontend/src/app/main/data/workspace/libraries.cljs index 6c5625a9e7..43d01b2763 100644 --- a/frontend/src/app/main/data/workspace/libraries.cljs +++ b/frontend/src/app/main/data/workspace/libraries.cljs @@ -1281,38 +1281,61 @@ file (dsh/lookup-file state file-id) file-data (get file :data) ignore-until (get file :ignore-sync-until) - permissions (:permissions state) + permissions (:permissions state) libraries-need-sync (->> (vals (get state :files)) (filter #(= (:library-of %) file-id)) - (filter #(seq (assets-need-sync % file-data ignore-until)))) + (filter #(seq (assets-need-sync % file-data ignore-until))))] - do-more-info - #(modal/show! :libraries-dialog {:starting-tab "updates" :file-id file-id}) + (if-not (and (:can-edit permissions) + (seq libraries-need-sync)) + ;; Fast path: no libraries need sync based on timestamps + (rx/empty) - do-update - #(do (apply st/emit! (map (fn [library] - (sync-file (:current-file-id state) - (:id library))) - libraries-need-sync)) - (st/emit! (ntf/hide))) + ;; Defer the expensive change generation check to avoid blocking the UI. + ;; For files with many libraries, this prevents stuttering/freezing. + (->> (rx/timer 0) + (rx/map (fn [_] + ;; This runs asynchronously on the next tick. + ;; Filter libraries to only those that would produce actual sync changes. + (let [libraries (dsh/lookup-libraries state)] + (filter (fn [library] + (seq (:redo-changes + (cll/generate-sync-file-changes + (pcb/empty-changes) + nil + nil + file-id + nil + (:id library) + libraries + file-id)))) + libraries-need-sync)))) + (rx/filter seq) + (rx/map (fn [libraries-with-changes] + (let [do-more-info + #(modal/show! :libraries-dialog {:starting-tab "updates" :file-id file-id}) - do-dismiss - #(st/emit! ignore-sync (ntf/hide))] + do-update + #(do (apply st/emit! (map (fn [library] + (sync-file file-id (:id library))) + libraries-with-changes)) + (st/emit! (ntf/hide))) - (when (and (:can-edit permissions) - (seq libraries-need-sync)) - (rx/of (ntf/dialog - :content (tr "workspace.updates.there-are-updates") - :controls :inline-actions - :links [{:label (tr "workspace.updates.more-info") - :callback do-more-info}] - :cancel {:label (tr "workspace.updates.dismiss") - :callback do-dismiss} - :accept {:label (tr "workspace.updates.update") - :callback do-update} - :tag :sync-dialog))))))) + do-dismiss + #(st/emit! ignore-sync (ntf/hide))] + + (ntf/dialog + :content (tr "workspace.updates.there-are-updates") + :controls :inline-actions + :links [{:label (tr "workspace.updates.more-info") + :callback do-more-info}] + :cancel {:label (tr "workspace.updates.dismiss") + :callback do-dismiss} + :accept {:label (tr "workspace.updates.update") + :callback do-update} + :tag :sync-dialog)))))))))) (defn touch-component "Update the modified-at attribute of the component to now"