mirror of
https://github.com/penpot/penpot.git
synced 2026-08-01 02:46:20 +00:00
🐛 Fix integrity error on copy-paste swapped component
When copying a component containing a swapped nested component, the copy operation was not preserving the swap information correctly. This caused referential integrity errors when pasting the component. The fix detects when a subinstance head's shape-ref is being advanced during copy and preserves the original shape-ref as a swap-slot. This ensures referential integrity is maintained when the copy is pasted in a context where the parent's near main is reachable. Closes #9547
This commit is contained in:
parent
ab284febf7
commit
5649ebc735
@ -5,6 +5,7 @@
|
||||
### :boom: Breaking changes & Deprecations
|
||||
|
||||
### :rocket: Epics and highlights
|
||||
|
||||
- WebGL rendering (beta) user preference [#9683](https://github.com/penpot/penpot/issues/9683) (PR:[9113](https://github.com/penpot/penpot/pull/9113))
|
||||
- Design Tokens at the design tab: numeric fields with token selection in place [#9358](https://github.com/penpot/penpot/issues/9358)
|
||||
|
||||
@ -64,6 +65,7 @@
|
||||
- Allow deleting the profile avatar after uploading (by @moorsecopers99) [#9067](https://github.com/penpot/penpot/issues/9067) (PR: [#9068](https://github.com/penpot/penpot/pull/9068))
|
||||
|
||||
### :bug: Bugs fixed
|
||||
|
||||
- Fix Alt/Option to draw shapes from center point (by @offreal) [#8360](https://github.com/penpot/penpot/issues/8360) (PR: [#8361](https://github.com/penpot/penpot/pull/8361))
|
||||
- Fix library update button freezing [#9330](https://github.com/penpot/penpot/issues/9330) (PR: [#9513](https://github.com/penpot/penpot/pull/9513))
|
||||
- Fix typo in subscription settings success key (by @jack-stormentswe) [#9203](https://github.com/penpot/penpot/issues/9203) (PR: [#9204](https://github.com/penpot/penpot/pull/9204))
|
||||
@ -125,6 +127,7 @@
|
||||
- Fix text fill color stops updating in multiselect with texts [#9608](https://github.com/penpot/penpot/issues/9608) (PR: [#9549](https://github.com/penpot/penpot/pull/9549))
|
||||
- Fix editing a legacy text element silently detaches its color token [Taiga #13958](https://tree.taiga.io/project/penpot/issue/13958)
|
||||
- Fix token application to grid paddings [Taiga #14136](https://tree.taiga.io/project/penpot/issue/14136)
|
||||
- Fix copying and pasting a swapped component causes a referential integrity error [Github #9547](https://github.com/penpot/penpot/issues/9547)
|
||||
|
||||
## 2.15.4 (Unreleased)
|
||||
|
||||
@ -149,7 +152,6 @@
|
||||
|
||||
- Fix mcp related internal config for docker images [GH #9565](https://github.com/penpot/penpot/pull/9565)
|
||||
|
||||
|
||||
## 2.15.1
|
||||
|
||||
### :sparkles: New features & Enhancements
|
||||
@ -160,7 +162,6 @@
|
||||
|
||||
- Fix "Help & Learning" submenu vertical alignment in account menu (by @juan-flores077) [#9137](https://github.com/penpot/penpot/issues/9137) (PR: [#9138](https://github.com/penpot/penpot/pull/9138))
|
||||
|
||||
|
||||
## 2.15.0
|
||||
|
||||
### :sparkles: New features & Enhancements
|
||||
|
||||
@ -170,13 +170,28 @@
|
||||
objects))
|
||||
|
||||
(advance-shape [file libraries page level-delta objects shape]
|
||||
(let [new-shape-ref (ctf/advance-shape-ref file page libraries shape level-delta {:include-deleted? true})
|
||||
container (ctn/make-container page :page)
|
||||
new-touched (ctf/get-touched-from-ref-chain-until-target-ref container libraries shape new-shape-ref)]
|
||||
(let [new-shape-ref (ctf/advance-shape-ref file page libraries shape level-delta {:include-deleted? true})
|
||||
container (ctn/make-container page :page)
|
||||
new-touched (ctf/get-touched-from-ref-chain-until-target-ref container libraries shape new-shape-ref)
|
||||
old-shape-ref (:shape-ref shape)
|
||||
ref-changed? (and (some? new-shape-ref) (not= new-shape-ref old-shape-ref))
|
||||
;; If we advance the shape-ref of a subinstance head, the new
|
||||
;; shape-ref will no longer match the parent component child
|
||||
;; at the same position, so the shape becomes "swapped" with
|
||||
;; respect to the enclosing instance. Preserve the original
|
||||
;; shape-ref as swap-slot so that referential integrity holds
|
||||
;; when the copy is pasted in a context where the parent's
|
||||
;; near main is reachable.
|
||||
needs-swap-slot? (and ref-changed?
|
||||
(ctc/subinstance-head? shape)
|
||||
(nil? (ctc/get-swap-slot shape)))]
|
||||
(cond-> objects
|
||||
(and (some? new-shape-ref) (not= new-shape-ref (:shape-ref shape)))
|
||||
ref-changed?
|
||||
(-> (assoc-in [(:id shape) :shape-ref] new-shape-ref)
|
||||
(assoc-in [(:id shape) :touched] new-touched)))))
|
||||
(assoc-in [(:id shape) :touched] new-touched))
|
||||
|
||||
needs-swap-slot?
|
||||
(update (:id shape) ctc/set-swap-slot old-shape-ref))))
|
||||
|
||||
(on-copy-error [error]
|
||||
(js/console.error "clipboard blocked:" error)
|
||||
|
||||
@ -247,10 +247,21 @@
|
||||
objects))
|
||||
|
||||
(advance-shape [file libraries page level-delta objects shape]
|
||||
(let [new-shape-ref (ctf/advance-shape-ref file page libraries shape level-delta {:include-deleted? true})]
|
||||
(let [new-shape-ref (ctf/advance-shape-ref file page libraries shape level-delta {:include-deleted? true})
|
||||
container (ctn/make-container page :page)
|
||||
new-touched (ctf/get-touched-from-ref-chain-until-target-ref container libraries shape new-shape-ref)
|
||||
old-shape-ref (:shape-ref shape)
|
||||
ref-changed? (and (some? new-shape-ref) (not= new-shape-ref old-shape-ref))
|
||||
needs-swap-slot? (and ref-changed?
|
||||
(ctk/subinstance-head? shape)
|
||||
(nil? (ctk/get-swap-slot shape)))]
|
||||
(cond-> objects
|
||||
(and (some? new-shape-ref) (not= new-shape-ref (:shape-ref shape)))
|
||||
(assoc-in [(:id shape) :shape-ref] new-shape-ref))))]
|
||||
ref-changed?
|
||||
(-> (assoc-in [(:id shape) :shape-ref] new-shape-ref)
|
||||
(assoc-in [(:id shape) :touched] new-touched))
|
||||
|
||||
needs-swap-slot?
|
||||
(update (:id shape) ctk/set-swap-slot old-shape-ref))))]
|
||||
|
||||
(let [file-id (:id file)
|
||||
frame-id (cfh/common-parent-frame objects selected)
|
||||
|
||||
@ -870,3 +870,73 @@
|
||||
(t/is (some? copied-blue1'))
|
||||
(t/is (nil? (ctk/get-swap-slot copied-blue1')))))))))
|
||||
|
||||
(t/deftest test-set-swap-slot-copy-paste-two-levels-nested-head
|
||||
;; Test that when copy-pasting a sub-instance head that contains a deeper sub-instance
|
||||
;; head (with no swap-slot), the advance-shape step correctly sets a swap-slot on the
|
||||
;; inner head. Without the fix this causes a backend :referential-integrity / :missing-slot
|
||||
;; validation error.
|
||||
(t/async
|
||||
done
|
||||
(let [;; ==== Setup
|
||||
;; {:main1-root} [:name Frame1] # [Component :comp1]
|
||||
;; :main1-child [:name Rect1]
|
||||
;;
|
||||
;; {:main2-root} [:name Frame2] # [Component :comp2]
|
||||
;; :nested-head1 [:name Frame1] @--> [Component :comp1] :main1-root
|
||||
;; <no-label> [:name Rect1] ---> :main1-child
|
||||
;;
|
||||
;; {:main3-root} [:name Frame3] # [Component :comp3]
|
||||
;; :nested-head2 [:name Frame2] @--> [Component :comp2] :main2-root
|
||||
;; :nested-subhead2 [:name Frame1] @--> [Component :comp1] :main1-root
|
||||
;; <no-label> [:name Rect1] ---> :main1-child
|
||||
;;
|
||||
;; :copy3-root [:name Frame3] #--> [Component :comp3] :main3-root
|
||||
;; <copy-head2> [:name Frame2] @--> [Component :comp2] :nested-head2 (no swap-slot)
|
||||
;; <copy-subhead1> [:name Frame1] @--> [Component :comp1] :nested-subhead2 (no swap-slot)
|
||||
;; <no-label> [:name Rect1] ---> <no-label>
|
||||
file (-> (cthf/sample-file :file1)
|
||||
(ctho/add-two-levels-nested-component-with-copy
|
||||
:comp1 :main1-root :main1-child
|
||||
:comp2 :main2-root :nested-head1
|
||||
:comp3 :main3-root :nested-head2 :nested-subhead2
|
||||
:copy3-root))
|
||||
store (ths/setup-store file)
|
||||
|
||||
;; ==== Action
|
||||
page (cthf/current-page file)
|
||||
copy3-root (cths/get-shape file :copy3-root)
|
||||
;; The copy of :nested-head2 inside copy3-root (sub-instance head of comp2, no swap-slot,
|
||||
;; nested 1 level deep — so level-delta=1 during copy-paste)
|
||||
copy-head2-id (first (:shapes copy3-root))
|
||||
copy-head2 (get (:objects page) copy-head2-id)
|
||||
features #{"components/v2"}
|
||||
version 46
|
||||
|
||||
pdata (thp/simulate-copy-shape #{copy-head2-id} (:objects page) {(:id file) file} page file features version)
|
||||
|
||||
events
|
||||
[(dws/select-shape uuid/zero)
|
||||
(dw/paste-shapes pdata)]]
|
||||
|
||||
(ths/run-store
|
||||
store done events
|
||||
(fn [new-state]
|
||||
(let [;; ==== Get
|
||||
file' (ths/get-file-from-state new-state)
|
||||
page' (cthf/current-page file')
|
||||
;; Find the pasted copy-head2 at the page root (parent = uuid/zero)
|
||||
pasted-head2' (find-copied-shape copy-head2 page' uuid/zero)
|
||||
;; Its first child is the copy of :nested-subhead2 (sub-instance head of comp1)
|
||||
pasted-subhead1-id' (some-> pasted-head2' :shapes first)
|
||||
pasted-subhead1' (get (:objects page') pasted-subhead1-id')]
|
||||
|
||||
;; ==== Check
|
||||
(t/is (some? pasted-head2'))
|
||||
(t/is (some? pasted-subhead1'))
|
||||
|
||||
;; The inner sub-instance head must have a swap-slot set because advance-shape
|
||||
;; changed its shape-ref (level-delta=1). The swap-slot records the pre-advance
|
||||
;; shape-ref so the backend can validate referential integrity.
|
||||
(t/is (some? (ctk/get-swap-slot pasted-subhead1'))
|
||||
"Pasted inner sub-instance head must have a swap-slot after advance-shape")))))))
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user