mirror of
https://github.com/penpot/penpot.git
synced 2026-07-02 12:25:42 +00:00
🐛 Fix problems with plugins API (#10412)
* ✨ Adds static dispatch safe stubs in tests * 🐛 Fix shapesColors metadata key to match ColorShapeInfo * 🐛 Fix CommentThread.remove rejecting the owner's own threads * 🐛 Fix page.removeCommentThread throwing on a spurious Promise * ✨ Implement ShapeBase.swapComponent in the plugin API * ✨ Expose File.revn in the plugin API * 🐛 Fix FileVersion.createdAt calling Luxon method on a js/Date * 🐛 Fix plugin font/typography application to text and ranges * 🐛 Default plugin overlay interaction position for non-manual types * 🐛 Fix plugin interaction setters passing an id-only shape * 🐛 Fix grid addColumnAtIndex rejecting valid track types * 🐛 Expose libraryId on library color/typography/component proxies * ✨ Implement LibraryTypography.setFont in the plugin API * 🐛 Fix typography.applyToTextRange reading unexposed range bounds * 🐛 Fix utils.geometry.center argument mismatch * 🐛 Fix localStorage.removeItem calling getItem * 🐛 Fix shape backgroundBlur proxy key casing * 🐛 Report boolean shape type as 'boolean' in the plugin API * 🐛 Return the resulting paths from plugin flatten * 🐛 Make plugin z-order methods act on the target shape * 🐛 Make is-variant-container? return a boolean * ✨ Implement Group.isMask in the plugin API * 🐛 Return a shape proxy from TextRange.shape * 🐛 Return the duplicated set from TokenSet.duplicate * 🐛 Fix theme addSet/removeSet reading set name with a keyword * 🐛 Accept string fontFamilies token value in the plugin API * 🐛 Fix combineAsVariants ignoring the passed component ids * 🐛 Fix board removeRulerGuide ignoring its argument * 🐛 Fix board guides setter schema and parser * 🐛 Avoid 0-byte allocation when syncing empty grid tracks * 🐛 Validate grid track indices in the plugin API * 🐛 Return null for empty input in group() and centerShapes() * 🐛 Return TokenTypographyValue[] from a typography token's resolvedValue * 🐛 Return TokenShadowValue[] from a shadow token's resolvedValue * 🐛 Return string[] from a fontFamilies token's resolvedValue * 🐛 Clear mutually-exclusive reps when setting LibraryColor gradient/image * 🐛 Add readonly tags to types, deprecate Image type * 📚 Update plugins changelog
This commit is contained in:
parent
99638fa60c
commit
f993f203bd
@ -249,7 +249,7 @@
|
||||
(defn is-variant-container?
|
||||
"Check if this shape is a variant container"
|
||||
[shape]
|
||||
(:is-variant-container shape))
|
||||
(boolean (:is-variant-container shape)))
|
||||
|
||||
(defn set-touched-group
|
||||
[touched group]
|
||||
|
||||
@ -772,44 +772,46 @@
|
||||
#{:up :down :bottom :top})
|
||||
|
||||
(defn vertical-order-selected
|
||||
[loc]
|
||||
(dm/assert!
|
||||
"expected valid location"
|
||||
(contains? valid-vertical-locations loc))
|
||||
(ptk/reify ::vertical-order-selected
|
||||
ptk/WatchEvent
|
||||
(watch [it state _]
|
||||
(let [page-id (:current-page-id state)
|
||||
objects (dsh/lookup-page-objects state page-id)
|
||||
selected-ids (dsh/lookup-selected state)
|
||||
selected-shapes (map (d/getf objects) selected-ids)
|
||||
undo-id (js/Symbol)
|
||||
([loc]
|
||||
(vertical-order-selected loc nil))
|
||||
([loc ids]
|
||||
(dm/assert!
|
||||
"expected valid location"
|
||||
(contains? valid-vertical-locations loc))
|
||||
(ptk/reify ::vertical-order-selected
|
||||
ptk/WatchEvent
|
||||
(watch [it state _]
|
||||
(let [page-id (:current-page-id state)
|
||||
objects (dsh/lookup-page-objects state page-id)
|
||||
selected-ids (or ids (dsh/lookup-selected state))
|
||||
selected-shapes (map (d/getf objects) selected-ids)
|
||||
undo-id (js/Symbol)
|
||||
|
||||
move-shape
|
||||
(fn [changes shape]
|
||||
(let [parent (get objects (:parent-id shape))
|
||||
sibling-ids (:shapes parent)
|
||||
current-index (d/index-of sibling-ids (:id shape))
|
||||
index-in-selection (d/index-of selected-ids (:id shape))
|
||||
new-index (case loc
|
||||
:top (count sibling-ids)
|
||||
:down (max 0 (- current-index 1))
|
||||
:up (min (count sibling-ids) (+ (inc current-index) 1))
|
||||
:bottom index-in-selection)]
|
||||
(pcb/change-parent changes
|
||||
(:id parent)
|
||||
[shape]
|
||||
new-index)))
|
||||
move-shape
|
||||
(fn [changes shape]
|
||||
(let [parent (get objects (:parent-id shape))
|
||||
sibling-ids (:shapes parent)
|
||||
current-index (d/index-of sibling-ids (:id shape))
|
||||
index-in-selection (d/index-of selected-ids (:id shape))
|
||||
new-index (case loc
|
||||
:top (count sibling-ids)
|
||||
:down (max 0 (- current-index 1))
|
||||
:up (min (count sibling-ids) (+ (inc current-index) 1))
|
||||
:bottom index-in-selection)]
|
||||
(pcb/change-parent changes
|
||||
(:id parent)
|
||||
[shape]
|
||||
new-index)))
|
||||
|
||||
changes (reduce move-shape
|
||||
(-> (pcb/empty-changes it page-id)
|
||||
(pcb/with-objects objects))
|
||||
selected-shapes)]
|
||||
changes (reduce move-shape
|
||||
(-> (pcb/empty-changes it page-id)
|
||||
(pcb/with-objects objects))
|
||||
selected-shapes)]
|
||||
|
||||
(rx/of (dwu/start-undo-transaction undo-id)
|
||||
(dch/commit-changes changes)
|
||||
(ptk/data-event :layout/update {:ids selected-ids})
|
||||
(dwu/commit-undo-transaction undo-id))))))
|
||||
(rx/of (dwu/start-undo-transaction undo-id)
|
||||
(dch/commit-changes changes)
|
||||
(ptk/data-event :layout/update {:ids selected-ids})
|
||||
(dwu/commit-undo-transaction undo-id)))))))
|
||||
|
||||
(defn set-shape-index
|
||||
[file-id page-id id new-index]
|
||||
|
||||
@ -343,20 +343,23 @@
|
||||
(dch/commit-changes changes))))))
|
||||
|
||||
(defn duplicate-token-set
|
||||
[id]
|
||||
(ptk/reify ::duplicate-token-set
|
||||
ptk/WatchEvent
|
||||
(watch [it state _]
|
||||
(let [data (dsh/lookup-file-data state)
|
||||
tokens-lib (get data :tokens-lib)
|
||||
suffix (tr "workspace.tokens.duplicate-suffix")]
|
||||
([id]
|
||||
(duplicate-token-set id nil))
|
||||
([id {:keys [id-ref]}]
|
||||
(ptk/reify ::duplicate-token-set
|
||||
ptk/WatchEvent
|
||||
(watch [it state _]
|
||||
(let [data (dsh/lookup-file-data state)
|
||||
tokens-lib (get data :tokens-lib)
|
||||
suffix (tr "workspace.tokens.duplicate-suffix")]
|
||||
|
||||
(when-let [token-set (ctob/duplicate-set id tokens-lib {:suffix suffix})]
|
||||
(let [changes (-> (pcb/empty-changes it)
|
||||
(pcb/with-library-data data)
|
||||
(pcb/set-token-set (ctob/get-id token-set) token-set))]
|
||||
(rx/of (set-selected-token-set-id (ctob/get-id token-set))
|
||||
(dch/commit-changes changes))))))))
|
||||
(when-let [token-set (ctob/duplicate-set id tokens-lib {:suffix suffix})]
|
||||
(when id-ref (reset! id-ref (ctob/get-id token-set)))
|
||||
(let [changes (-> (pcb/empty-changes it)
|
||||
(pcb/with-library-data data)
|
||||
(pcb/set-token-set (ctob/get-id token-set) token-set))]
|
||||
(rx/of (set-selected-token-set-id (ctob/get-id token-set))
|
||||
(dch/commit-changes changes)))))))))
|
||||
|
||||
(defn set-enabled-token-set
|
||||
[name enabled?]
|
||||
|
||||
@ -317,6 +317,11 @@
|
||||
(or (not (array? shapes)) (not (every? shape/shape-proxy? shapes)))
|
||||
(u/not-valid plugin-id :group-shapes shapes)
|
||||
|
||||
;; A group cannot be created from no shapes; per the documented contract
|
||||
;; return null instead of a proxy pointing at a shape that never exists.
|
||||
(zero? (alength shapes))
|
||||
nil
|
||||
|
||||
(some #(not (u/page-active? (obj/get % "$page"))) shapes)
|
||||
(u/not-valid plugin-id :group "Cannot modify a page that is not currently active")
|
||||
|
||||
@ -664,8 +669,13 @@
|
||||
(u/not-valid plugin-id :flatten-shapes "Not valid shapes")
|
||||
|
||||
:else
|
||||
(let [ids (into #{} (map #(obj/get % "$id")) shapes)]
|
||||
(st/emit! (dw/convert-selected-to-path ids)))))
|
||||
;; convert-selected-to-path converts the shapes in place (keeping their
|
||||
;; ids), so return proxies for the same ids, now resolving as paths.
|
||||
(let [file-id (:current-file-id @st/state)
|
||||
page-id (:current-page-id @st/state)
|
||||
ids (mapv #(obj/get % "$id") shapes)]
|
||||
(st/emit! (dw/convert-selected-to-path (into #{} ids)))
|
||||
(apply array (map #(shape/shape-proxy plugin-id file-id page-id %) ids)))))
|
||||
|
||||
:createVariantFromComponents
|
||||
(fn [shapes]
|
||||
|
||||
@ -9,7 +9,6 @@
|
||||
[app.common.geom.point :as gpt]
|
||||
[app.common.schema :as sm]
|
||||
[app.main.data.comments :as dc]
|
||||
[app.main.data.helpers :as dsh]
|
||||
[app.main.data.workspace.comments :as dwc]
|
||||
[app.main.repo :as rp]
|
||||
[app.main.store :as st]
|
||||
@ -203,13 +202,12 @@
|
||||
|
||||
:remove
|
||||
(fn []
|
||||
(let [profile (:profile @st/state)
|
||||
owner (dsh/lookup-profile @st/state (:owner-id data))]
|
||||
(let [profile (:profile @st/state)]
|
||||
(cond
|
||||
(not (r/check-permission plugin-id "comment:write"))
|
||||
(u/not-valid plugin-id :remove "Plugin doesn't have 'comment:write' permission")
|
||||
|
||||
(not= (:id profile) owner)
|
||||
(not= (:id profile) (:owner-id data))
|
||||
(u/not-valid plugin-id :remove "Cannot change content from another user's comments")
|
||||
|
||||
:else
|
||||
|
||||
@ -64,7 +64,7 @@
|
||||
(user/user-proxy plugin-id user-data)))}
|
||||
|
||||
:createdAt
|
||||
{:get #(.toJSDate ^js (:created-at @data))}
|
||||
{:get #(:created-at @data)}
|
||||
|
||||
:isAutosave
|
||||
{:get #(= "system" (:created-by @data))}
|
||||
@ -136,6 +136,9 @@
|
||||
:name
|
||||
{:get #(-> (u/locate-file id) :name)}
|
||||
|
||||
:revn
|
||||
{:get #(-> (u/locate-file id) :revn)}
|
||||
|
||||
:pages
|
||||
{:this true
|
||||
:get #(.getPages ^js %)}
|
||||
|
||||
@ -64,13 +64,13 @@
|
||||
(u/not-valid plugin-id :applyToText "Cannot modify a page that is not currently active")
|
||||
|
||||
:else
|
||||
(let [id (obj/get text "$id")
|
||||
(let [text-id (obj/get text "$id")
|
||||
values {:font-id id
|
||||
:font-family family
|
||||
:font-style (d/nilv (obj/get variant "fontStyle") (:style default-variant))
|
||||
:font-variant-id (d/nilv (obj/get variant "fontVariantId") (:id default-variant))
|
||||
:font-weight (d/nilv (obj/get variant "fontWeight") (:weight default-variant))}]
|
||||
(st/emit! (dwt/update-attrs id values)))))
|
||||
(st/emit! (dwt/update-attrs text-id values)))))
|
||||
|
||||
:applyToRange
|
||||
(fn [range variant]
|
||||
@ -85,15 +85,15 @@
|
||||
(u/not-valid plugin-id :applyToRange "Cannot modify a page that is not currently active")
|
||||
|
||||
:else
|
||||
(let [id (obj/get range "$id")
|
||||
start (obj/get range "start")
|
||||
end (obj/get range "end")
|
||||
(let [range-id (obj/get range "$id")
|
||||
start (obj/get range "$start")
|
||||
end (obj/get range "$end")
|
||||
values {:font-id id
|
||||
:font-family family
|
||||
:font-style (d/nilv (obj/get variant "fontStyle") (:style default-variant))
|
||||
:font-variant-id (d/nilv (obj/get variant "fontVariantId") (:id default-variant))
|
||||
:font-weight (d/nilv (obj/get variant "fontWeight") (:weight default-variant))}]
|
||||
(st/emit! (dwt/update-text-range id start end values)))))))))
|
||||
(st/emit! (dwt/update-text-range range-id start end values)))))))))
|
||||
|
||||
(defn fonts-subcontext
|
||||
[plugin-id]
|
||||
|
||||
@ -47,6 +47,7 @@
|
||||
:frame "board"
|
||||
:rect "rectangle"
|
||||
:circle "ellipse"
|
||||
:bool "boolean"
|
||||
(d/name type)))
|
||||
|
||||
;;export type Bounds = {
|
||||
@ -146,7 +147,7 @@
|
||||
[[color attrs]]
|
||||
(let [shapes-info (apply array (map format-shape-info attrs))
|
||||
color (format-color color)]
|
||||
(obj/set! color "shapeInfo" shapes-info)
|
||||
(obj/set! color "shapesInfo" shapes-info)
|
||||
color))
|
||||
|
||||
|
||||
|
||||
@ -301,11 +301,15 @@
|
||||
|
||||
:addRowAtIndex
|
||||
(fn [index type value]
|
||||
(let [type (keyword type)]
|
||||
(let [type (keyword type)
|
||||
num-rows (-> (u/locate-shape file-id page-id id) :layout-grid-rows count)]
|
||||
(cond
|
||||
(not (sm/valid-safe-int? index))
|
||||
(u/not-valid plugin-id :addRowAtIndex-index index)
|
||||
|
||||
(or (< index 0) (> index num-rows))
|
||||
(u/not-valid plugin-id :addRowAtIndex-index index)
|
||||
|
||||
(not (contains? ctl/grid-track-types type))
|
||||
(u/not-valid plugin-id :addRowAtIndex-type type)
|
||||
|
||||
@ -344,64 +348,80 @@
|
||||
|
||||
:addColumnAtIndex
|
||||
(fn [index type value]
|
||||
(cond
|
||||
(not (sm/valid-safe-int? index))
|
||||
(u/not-valid plugin-id :addColumnAtIndex-index index)
|
||||
(let [type (keyword type)
|
||||
num-columns (-> (u/locate-shape file-id page-id id) :layout-grid-columns count)]
|
||||
(cond
|
||||
(not (sm/valid-safe-int? index))
|
||||
(u/not-valid plugin-id :addColumnAtIndex-index index)
|
||||
|
||||
(not (contains? ctl/grid-track-types type))
|
||||
(u/not-valid plugin-id :addColumnAtIndex-type type)
|
||||
(or (< index 0) (> index num-columns))
|
||||
(u/not-valid plugin-id :addColumnAtIndex-index index)
|
||||
|
||||
(and (or (= :percent type) (= :flex type) (= :fixed type))
|
||||
(not (sm/valid-safe-number? value)))
|
||||
(u/not-valid plugin-id :addColumnAtIndex-value value)
|
||||
(not (contains? ctl/grid-track-types type))
|
||||
(u/not-valid plugin-id :addColumnAtIndex-type type)
|
||||
|
||||
(not (r/check-permission plugin-id "content:write"))
|
||||
(u/not-valid plugin-id :addColumnAtIndex "Plugin doesn't have 'content:write' permission")
|
||||
(and (or (= :percent type) (= :flex type) (= :fixed type))
|
||||
(not (sm/valid-safe-number? value)))
|
||||
(u/not-valid plugin-id :addColumnAtIndex-value value)
|
||||
|
||||
(not (u/page-active? page-id))
|
||||
(u/not-valid plugin-id :addColumnAtIndex "Cannot modify a page that is not currently active")
|
||||
(not (r/check-permission plugin-id "content:write"))
|
||||
(u/not-valid plugin-id :addColumnAtIndex "Plugin doesn't have 'content:write' permission")
|
||||
|
||||
:else
|
||||
(let [type (keyword type)]
|
||||
(not (u/page-active? page-id))
|
||||
(u/not-valid plugin-id :addColumnAtIndex "Cannot modify a page that is not currently active")
|
||||
|
||||
:else
|
||||
(st/emit! (dwsl/add-layout-track #{id} :column {:type type :value value} index)))))
|
||||
|
||||
:removeRow
|
||||
(fn [index]
|
||||
(cond
|
||||
(not (sm/valid-safe-int? index))
|
||||
(u/not-valid plugin-id :removeRow index)
|
||||
(let [num-rows (-> (u/locate-shape file-id page-id id) :layout-grid-rows count)]
|
||||
(cond
|
||||
(not (sm/valid-safe-int? index))
|
||||
(u/not-valid plugin-id :removeRow index)
|
||||
|
||||
(not (r/check-permission plugin-id "content:write"))
|
||||
(u/not-valid plugin-id :removeRow "Plugin doesn't have 'content:write' permission")
|
||||
(or (< index 0) (>= index num-rows))
|
||||
(u/not-valid plugin-id :removeRow index)
|
||||
|
||||
(not (u/page-active? page-id))
|
||||
(u/not-valid plugin-id :removeRow "Cannot modify a page that is not currently active")
|
||||
(not (r/check-permission plugin-id "content:write"))
|
||||
(u/not-valid plugin-id :removeRow "Plugin doesn't have 'content:write' permission")
|
||||
|
||||
:else
|
||||
(st/emit! (dwsl/remove-layout-track #{id} :row index))))
|
||||
(not (u/page-active? page-id))
|
||||
(u/not-valid plugin-id :removeRow "Cannot modify a page that is not currently active")
|
||||
|
||||
:else
|
||||
(st/emit! (dwsl/remove-layout-track #{id} :row index)))))
|
||||
|
||||
:removeColumn
|
||||
(fn [index]
|
||||
(cond
|
||||
(not (sm/valid-safe-int? index))
|
||||
(u/not-valid plugin-id :removeColumn index)
|
||||
(let [num-columns (-> (u/locate-shape file-id page-id id) :layout-grid-columns count)]
|
||||
(cond
|
||||
(not (sm/valid-safe-int? index))
|
||||
(u/not-valid plugin-id :removeColumn index)
|
||||
|
||||
(not (r/check-permission plugin-id "content:write"))
|
||||
(u/not-valid plugin-id :removeColumn "Plugin doesn't have 'content:write' permission")
|
||||
(or (< index 0) (>= index num-columns))
|
||||
(u/not-valid plugin-id :removeColumn index)
|
||||
|
||||
(not (u/page-active? page-id))
|
||||
(u/not-valid plugin-id :removeColumn "Cannot modify a page that is not currently active")
|
||||
(not (r/check-permission plugin-id "content:write"))
|
||||
(u/not-valid plugin-id :removeColumn "Plugin doesn't have 'content:write' permission")
|
||||
|
||||
:else
|
||||
(st/emit! (dwsl/remove-layout-track #{id} :column index))))
|
||||
(not (u/page-active? page-id))
|
||||
(u/not-valid plugin-id :removeColumn "Cannot modify a page that is not currently active")
|
||||
|
||||
:else
|
||||
(st/emit! (dwsl/remove-layout-track #{id} :column index)))))
|
||||
|
||||
:setColumn
|
||||
(fn [index type value]
|
||||
(let [type (keyword type)]
|
||||
(let [type (keyword type)
|
||||
num-columns (-> (u/locate-shape file-id page-id id) :layout-grid-columns count)]
|
||||
(cond
|
||||
(not (sm/valid-safe-int? index))
|
||||
(u/not-valid plugin-id :setColumn-index index)
|
||||
|
||||
(or (< index 0) (>= index num-columns))
|
||||
(u/not-valid plugin-id :setColumn-index index)
|
||||
|
||||
(not (contains? ctl/grid-track-types type))
|
||||
(u/not-valid plugin-id :setColumn-type type)
|
||||
|
||||
@ -420,11 +440,15 @@
|
||||
|
||||
:setRow
|
||||
(fn [index type value]
|
||||
(let [type (keyword type)]
|
||||
(let [type (keyword type)
|
||||
num-rows (-> (u/locate-shape file-id page-id id) :layout-grid-rows count)]
|
||||
(cond
|
||||
(not (sm/valid-safe-int? index))
|
||||
(u/not-valid plugin-id :setRow-index index)
|
||||
|
||||
(or (< index 0) (>= index num-rows))
|
||||
(u/not-valid plugin-id :setRow-index index)
|
||||
|
||||
(not (contains? ctl/grid-track-types type))
|
||||
(u/not-valid plugin-id :setRow-type type)
|
||||
|
||||
|
||||
@ -51,6 +51,7 @@
|
||||
:$file {:enumerable false :get (constantly file-id)}
|
||||
|
||||
:id {:get (fn [] (dm/str id))}
|
||||
:libraryId {:get (fn [] (dm/str file-id))}
|
||||
:fileId {:get #(dm/str file-id)}
|
||||
|
||||
:name
|
||||
@ -101,7 +102,8 @@
|
||||
|
||||
:else
|
||||
(let [color (-> (u/proxy->library-color self)
|
||||
(assoc :color value))]
|
||||
(assoc :color value)
|
||||
(dissoc :gradient :image))]
|
||||
(st/emit! (dwl/update-color-data color file-id)))))}
|
||||
|
||||
:opacity
|
||||
@ -136,7 +138,8 @@
|
||||
|
||||
:else
|
||||
(let [color (-> (u/proxy->library-color self)
|
||||
(assoc :gradient value))]
|
||||
(assoc :gradient value)
|
||||
(dissoc :color :image))]
|
||||
(st/emit! (dwl/update-color-data color file-id))))))}
|
||||
|
||||
:image
|
||||
@ -154,7 +157,8 @@
|
||||
|
||||
:else
|
||||
(let [color (-> (u/proxy->library-color self)
|
||||
(assoc :image value))]
|
||||
(assoc :image value)
|
||||
(dissoc :color :gradient))]
|
||||
(st/emit! (dwl/update-color-data color file-id))))))}
|
||||
|
||||
:remove
|
||||
@ -295,6 +299,7 @@
|
||||
:$id {:enumerable false :get (constantly id)}
|
||||
:$file {:enumerable false :get (constantly file-id)}
|
||||
:id {:get (fn [] (dm/str id))}
|
||||
:libraryId {:get (fn [] (dm/str file-id))}
|
||||
|
||||
:name
|
||||
{:this true
|
||||
@ -484,6 +489,27 @@
|
||||
(assoc :text-transform value))]
|
||||
(st/emit! (dwl/update-typography typo file-id)))))}
|
||||
|
||||
:setFont
|
||||
(fn [font variant]
|
||||
(cond
|
||||
(not (obj/type-of? font "FontProxy"))
|
||||
(u/not-valid plugin-id :setFont font)
|
||||
|
||||
(not (r/check-permission plugin-id "library:write"))
|
||||
(u/not-valid plugin-id :setFont "Plugin doesn't have 'library:write' permission")
|
||||
|
||||
:else
|
||||
;; When a variant is given read the variant-specific fields from it;
|
||||
;; otherwise the FontProxy exposes the font's default variant fields.
|
||||
(let [source (if (obj/type-of? variant "FontVariantProxy") variant font)
|
||||
typo (-> (u/locate-library-typography file-id id)
|
||||
(assoc :font-id (obj/get font "fontId")
|
||||
:font-family (obj/get font "fontFamily")
|
||||
:font-variant-id (obj/get source "fontVariantId")
|
||||
:font-style (obj/get source "fontStyle")
|
||||
:font-weight (obj/get source "fontWeight")))]
|
||||
(st/emit! (dwl/update-typography typo file-id)))))
|
||||
|
||||
:remove
|
||||
(fn []
|
||||
(cond
|
||||
@ -539,8 +565,8 @@
|
||||
|
||||
:else
|
||||
(let [shape-id (obj/get range "$id")
|
||||
start (obj/get range "start")
|
||||
end (obj/get range "end")
|
||||
start (obj/get range "$start")
|
||||
end (obj/get range "$end")
|
||||
typography (u/locate-library-typography file-id id)
|
||||
attrs (-> typography
|
||||
(assoc :typography-ref-file file-id)
|
||||
@ -718,6 +744,7 @@
|
||||
:$id {:enumerable false :get (constantly id)}
|
||||
:$file {:enumerable false :get (constantly file-id)}
|
||||
:id {:get (fn [] (dm/str id))}
|
||||
:libraryId {:get (fn [] (dm/str file-id))}
|
||||
|
||||
:name
|
||||
{:this true
|
||||
|
||||
@ -60,7 +60,7 @@
|
||||
(u/not-valid plugin-id :removeItem "The key must be a string")
|
||||
|
||||
:else
|
||||
(.getItem ^js local-storage (prefix-key plugin-id key))))
|
||||
(.removeItem ^js local-storage (prefix-key plugin-id key))))
|
||||
|
||||
:getKeys
|
||||
(fn []
|
||||
|
||||
@ -412,8 +412,7 @@
|
||||
(js/Promise.
|
||||
(fn [resolve]
|
||||
(let [thread-id (obj/get thread "$id")]
|
||||
(js/Promise.
|
||||
(st/emit! (dc/delete-comment-thread-on-workspace {:id thread-id} #(resolve)))))))))
|
||||
(st/emit! (dc/delete-comment-thread-on-workspace {:id thread-id} #(resolve))))))))
|
||||
|
||||
:findCommentThreads
|
||||
(fn [criteria]
|
||||
|
||||
@ -343,10 +343,10 @@
|
||||
(when (some? guide)
|
||||
(case (obj/get guide "type")
|
||||
"column"
|
||||
parse-frame-guide-column
|
||||
(parse-frame-guide-column guide)
|
||||
|
||||
"row"
|
||||
parse-frame-guide-row
|
||||
(parse-frame-guide-row guide)
|
||||
|
||||
"square"
|
||||
(parse-frame-guide-square guide))))
|
||||
@ -489,7 +489,7 @@
|
||||
:destination (-> (obj/get action "destination") (obj/get "$id"))
|
||||
:relative-to (-> (obj/get action "relativeTo") (obj/get "$id"))
|
||||
:overlay-pos-type (-> (obj/get action "position") parse-keyword)
|
||||
:overlay-position (-> (obj/get action "manualPositionLocation") parse-point)
|
||||
:overlay-position (-> (obj/get action "manualPositionLocation") parse-point (d/nilv (gpt/point 0 0)))
|
||||
:close-click-outside (obj/get action "closeWhenClickOutside")
|
||||
:background-overlay (obj/get action "addBackgroundOverlay")
|
||||
:animation (-> (obj/get action "animation") parse-animation)}
|
||||
|
||||
@ -14,10 +14,15 @@
|
||||
[app.plugins.utils :as u]))
|
||||
|
||||
(defn ^:export centerShapes
|
||||
[plugin-id shapes]
|
||||
[shapes]
|
||||
(cond
|
||||
(not (every? shape/shape-proxy? shapes))
|
||||
(u/not-valid plugin-id :centerShapes shapes)
|
||||
(u/not-valid nil :centerShapes shapes)
|
||||
|
||||
;; The documented contract returns null for an empty array; without this
|
||||
;; guard `shapes->rect` yields a non-rect and `rect->center` asserts.
|
||||
(empty? shapes)
|
||||
nil
|
||||
|
||||
:else
|
||||
(let [shapes (->> shapes (map u/proxy->shape))]
|
||||
|
||||
@ -101,7 +101,7 @@
|
||||
|
||||
:else
|
||||
(st/emit! (dwi/update-interaction
|
||||
{:id shape-id}
|
||||
(u/locate-shape file-id page-id shape-id)
|
||||
index
|
||||
#(assoc % :event-type value)
|
||||
{:page-id page-id})))))}
|
||||
@ -117,7 +117,7 @@
|
||||
|
||||
:else
|
||||
(st/emit! (dwi/update-interaction
|
||||
{:id shape-id}
|
||||
(u/locate-shape file-id page-id shape-id)
|
||||
index
|
||||
#(assoc % :delay value)
|
||||
{:page-id page-id}))))}
|
||||
@ -137,7 +137,7 @@
|
||||
|
||||
:else
|
||||
(st/emit! (dwi/update-interaction
|
||||
{:id shape-id}
|
||||
(u/locate-shape file-id page-id shape-id)
|
||||
index
|
||||
#(d/patch-object % params)
|
||||
{:page-id page-id})))))}
|
||||
@ -592,7 +592,7 @@
|
||||
:else
|
||||
(st/emit! (dwsh/update-shapes [id] #(assoc % :blur value)))))))}
|
||||
|
||||
:background-blur
|
||||
:backgroundBlur
|
||||
{:this true
|
||||
:get #(-> % u/proxy->shape :background-blur format/format-blur)
|
||||
:set
|
||||
@ -1249,6 +1249,11 @@
|
||||
:else
|
||||
(st/emit! (dwg/unmask-group #{id})))))
|
||||
|
||||
:isMask
|
||||
(fn []
|
||||
(let [shape (u/locate-shape file-id page-id id)]
|
||||
(boolean (cfh/mask-shape? shape))))
|
||||
|
||||
;; Only for path and bool shapes
|
||||
:toD
|
||||
(fn []
|
||||
@ -1315,19 +1320,19 @@
|
||||
|
||||
:bringForward
|
||||
(fn []
|
||||
(st/emit! (dw/vertical-order-selected :up)))
|
||||
(st/emit! (dw/vertical-order-selected :up [id])))
|
||||
|
||||
:sendBackward
|
||||
(fn []
|
||||
(st/emit! (dw/vertical-order-selected :down)))
|
||||
(st/emit! (dw/vertical-order-selected :down [id])))
|
||||
|
||||
:bringToFront
|
||||
(fn []
|
||||
(st/emit! (dw/vertical-order-selected :top)))
|
||||
(st/emit! (dw/vertical-order-selected :top [id])))
|
||||
|
||||
:sendToBack
|
||||
(fn []
|
||||
(st/emit! (dw/vertical-order-selected :bottom)))
|
||||
(st/emit! (dw/vertical-order-selected :bottom [id])))
|
||||
|
||||
;; COMPONENTS
|
||||
:isComponentInstance
|
||||
@ -1402,6 +1407,28 @@
|
||||
:else
|
||||
(st/emit! (dwl/detach-component id))))
|
||||
|
||||
:swapComponent
|
||||
(fn [component]
|
||||
(let [shape (u/locate-shape file-id page-id id)]
|
||||
(cond
|
||||
(not (u/page-active? page-id))
|
||||
(u/not-valid plugin-id :swapComponent "Cannot modify a page that is not currently active")
|
||||
|
||||
(not (r/check-permission plugin-id "content:write"))
|
||||
(u/not-valid plugin-id :swapComponent "Plugin doesn't have 'content:write' permission")
|
||||
|
||||
(not (obj/type-of? component "LibraryComponentProxy"))
|
||||
(u/not-valid plugin-id :swapComponent "Component not valid")
|
||||
|
||||
(not (ctk/in-component-copy? shape))
|
||||
(u/not-valid plugin-id :swapComponent "The shape is not a component copy instance")
|
||||
|
||||
:else
|
||||
(st/emit! (dwl/component-swap shape
|
||||
(obj/get component "$file")
|
||||
(obj/get component "$id")
|
||||
true)))))
|
||||
|
||||
;; Export
|
||||
:export
|
||||
(fn [value]
|
||||
@ -1536,7 +1563,7 @@
|
||||
(rg/ruler-guide-proxy plugin-id file-id page-id ruler-id)))))
|
||||
|
||||
:removeRulerGuide
|
||||
(fn [_ value]
|
||||
(fn [value]
|
||||
(cond
|
||||
(not (rg/ruler-guide-proxy? value))
|
||||
(u/not-valid plugin-id :removeRulerGuide "Guide not provided")
|
||||
@ -1618,7 +1645,7 @@
|
||||
|
||||
:else
|
||||
(let [ids
|
||||
(into #{id} (keep uuid/parse*) id)
|
||||
(into #{id} (keep uuid/parse*) ids)
|
||||
|
||||
valid?
|
||||
(every?
|
||||
@ -1740,7 +1767,7 @@
|
||||
(let [id (obj/get self "$id")
|
||||
value (parser/parse-frame-guides value)]
|
||||
(cond
|
||||
(not (sm/validate [:vector ::ctg/grid] value))
|
||||
(not (sm/validate [:vector ctg/schema:grid] value))
|
||||
(u/not-valid plugin-id :guides value)
|
||||
|
||||
(not (r/check-permission plugin-id "content:write"))
|
||||
|
||||
@ -78,7 +78,7 @@
|
||||
taking? (or taking? (and (<= from start) (< start to)))
|
||||
text (subs text (max 0 (- start acc)) (- end acc))
|
||||
result (cond-> result
|
||||
(and taking? (d/not-empty? text))
|
||||
(and taking? (seq text))
|
||||
(conj (assoc node-style :text text)))
|
||||
continue? (or (> from end) (>= end to))]
|
||||
(recur (when continue? (rest styles)) taking? to result))
|
||||
@ -95,10 +95,11 @@
|
||||
:$id {:enumerable false :get (constantly id)}
|
||||
:$file {:enumerable false :get (constantly file-id)}
|
||||
:$page {:enumerable false :get (constantly page-id)}
|
||||
:$start {:enumerable false :get (constantly start)}
|
||||
:$end {:enumerable false :get (constantly end)}
|
||||
|
||||
:shape
|
||||
{:this true
|
||||
:get #(-> % u/proxy->shape)}
|
||||
{:get (fn [] (format/shape-proxy plugin-id file-id page-id id))}
|
||||
|
||||
:characters
|
||||
{:this true
|
||||
|
||||
@ -96,13 +96,83 @@
|
||||
:expand-with-children false})
|
||||
(se/add-event plugin-id))))))
|
||||
|
||||
(defn- typography-resolved-value->js
|
||||
"Converts a resolved typography composite (a Clojure map keyed by the
|
||||
tokenscript field names) into the plugin's `TokenTypographyValue[]` shape: a
|
||||
JS array with a single object using the public camelCase member names."
|
||||
[m]
|
||||
(when (map? m)
|
||||
#js [#js {"fontFamilies" (clj->js (:font-family m))
|
||||
"fontSizes" (:font-size m)
|
||||
"fontWeights" (some-> (:font-weight m) str)
|
||||
"letterSpacing" (:letter-spacing m)
|
||||
"lineHeight" (:line-height m)
|
||||
"textCase" (:text-case m)
|
||||
"textDecoration" (:text-decoration m)}]))
|
||||
|
||||
(defn- shadow-key->camel
|
||||
"Renames a shadow composite field name (kebab string) to its public camelCase
|
||||
member name. The shadow schema is closed; offset-x/offset-y are its only
|
||||
multi-word fields, so the rest (blur, spread, color, inset) pass through."
|
||||
[k]
|
||||
(case k
|
||||
"offset-x" "offsetX"
|
||||
"offset-y" "offsetY"
|
||||
k))
|
||||
|
||||
(defn- shadow-entry->js
|
||||
"Converts one resolved shadow entry (a JS Map of field name -> tokenscript
|
||||
symbol) into a plain JS object using the public member names and the
|
||||
unit-converted values."
|
||||
[^js m]
|
||||
(let [out #js {}]
|
||||
(.forEach m (fn [sym k]
|
||||
(obj/set! out (shadow-key->camel k)
|
||||
(ts/tokenscript-symbols->penpot-unit sym))))
|
||||
out))
|
||||
|
||||
(defn- shadow-resolved-value->js
|
||||
"Converts a resolved shadow composite (a sequence of shadow entries) into the
|
||||
plugin's `TokenShadowValue[]` shape."
|
||||
[entries]
|
||||
(when (some? entries)
|
||||
(into-array (map shadow-entry->js entries))))
|
||||
|
||||
(defn- font-families-resolved-value->js
|
||||
"Converts a resolved fontFamilies value (a tokenscript list symbol) into the
|
||||
documented `string[]` shape rather than leaking the raw tokenscript structure."
|
||||
[resolved-value]
|
||||
(let [v (ts/tokenscript-symbols->penpot-unit resolved-value)]
|
||||
(cond
|
||||
(nil? v) nil
|
||||
(sequential? v) (clj->js v)
|
||||
:else #js [v])))
|
||||
|
||||
(defn- get-resolved-value
|
||||
[token tokens-tree]
|
||||
(let [resolved-tokens (ts/resolve-tokens tokens-tree)
|
||||
resolved-value (-> resolved-tokens
|
||||
(dm/get-in [(:name token) :resolved-value])
|
||||
(ts/tokenscript-symbols->penpot-unit))]
|
||||
resolved-value))
|
||||
resolved-value (dm/get-in resolved-tokens [(:name token) :resolved-value])]
|
||||
(cond
|
||||
(= :font-family (:type token))
|
||||
;; A fontFamilies token resolves to a list of families; expose it as the
|
||||
;; documented `string[]` rather than the raw tokenscript list symbol.
|
||||
(font-families-resolved-value->js resolved-value)
|
||||
|
||||
(= :typography (:type token))
|
||||
;; A typography token resolves to a composite; expose it as the documented
|
||||
;; `TokenTypographyValue[]` rather than the raw tokenscript structure.
|
||||
(typography-resolved-value->js
|
||||
(ts/tokenscript-symbols->penpot-unit resolved-value))
|
||||
|
||||
(= :shadow (:type token))
|
||||
;; A shadow token resolves to a list of composites whose entries the
|
||||
;; tokenscript unit conversion leaves as raw symbols; expose them as the
|
||||
;; documented `TokenShadowValue[]`.
|
||||
(shadow-resolved-value->js
|
||||
(ts/tokenscript-symbols->penpot-unit resolved-value))
|
||||
|
||||
:else
|
||||
(ts/tokenscript-symbols->penpot-unit resolved-value))))
|
||||
|
||||
(defn token-proxy? [p]
|
||||
(obj/type-of? p "TokenProxy"))
|
||||
@ -150,11 +220,21 @@
|
||||
(fn [_]
|
||||
(let [token (u/locate-token file-id set-id id)]
|
||||
(json/->js (:value token))))
|
||||
:schema (let [token (u/locate-token file-id set-id id)]
|
||||
(cfo/make-token-value-schema (:type token)))
|
||||
:schema (let [token (u/locate-token file-id set-id id)
|
||||
base (cfo/make-token-value-schema (:type token))]
|
||||
;; plugin-types declares the fontFamilies value as
|
||||
;; `string | string[]`, but the core schema only accepts a
|
||||
;; vector/ref; also accept a plain string (normalized in :set).
|
||||
(if (= :font-family (:type token))
|
||||
[:or :string base]
|
||||
base))
|
||||
:set
|
||||
(fn [_ value]
|
||||
(st/emit! (dwtl/update-token set-id id {:value value})))}
|
||||
(let [token (u/locate-token file-id set-id id)
|
||||
value (cond-> value
|
||||
(= :font-family (:type token))
|
||||
(ctob/convert-dtcg-font-family))]
|
||||
(st/emit! (dwtl/update-token set-id id {:value value}))))}
|
||||
|
||||
:resolvedValue
|
||||
{:this true
|
||||
@ -361,7 +441,10 @@
|
||||
|
||||
:duplicate
|
||||
(fn []
|
||||
(st/emit! (dwtl/duplicate-token-set id)))
|
||||
(let [id-ref (atom nil)]
|
||||
(st/emit! (dwtl/duplicate-token-set id {:id-ref id-ref}))
|
||||
(when (some? @id-ref)
|
||||
(token-set-proxy plugin-id file-id @id-ref))))
|
||||
|
||||
:remove
|
||||
(fn []
|
||||
@ -460,7 +543,7 @@
|
||||
;; Guard against nil to prevent `enable-set` from conj'ing nil
|
||||
;; into the theme's :sets — which would send `:sets #{nil}` to the
|
||||
;; backend and crash the workspace.
|
||||
(let [set-name (obj/get token-set :name)
|
||||
(let [set-name (obj/get token-set "name")
|
||||
theme (u/locate-token-theme file-id id)]
|
||||
(when (and (some? set-name) (some? theme))
|
||||
(st/emit! (dwtl/update-token-theme id (ctob/enable-set theme set-name))))))}
|
||||
@ -470,7 +553,7 @@
|
||||
:schema [:tuple [:fn token-set-proxy?]]
|
||||
:fn (fn [token-set]
|
||||
;; Same nil guard as addSet — see comment above.
|
||||
(let [set-name (obj/get token-set :name)
|
||||
(let [set-name (obj/get token-set "name")
|
||||
theme (u/locate-token-theme file-id id)]
|
||||
(when (and (some? set-name) (some? theme))
|
||||
(st/emit! (dwtl/update-token-theme id (ctob/disable-set theme set-name))))))}
|
||||
|
||||
@ -1074,66 +1074,75 @@
|
||||
|
||||
(defn set-grid-layout-rows
|
||||
[entries]
|
||||
(let [size (mem/get-alloc-size entries GRID-LAYOUT-ROW-U8-SIZE)
|
||||
offset (mem/alloc size)
|
||||
dview (mem/get-data-view)]
|
||||
;; Only allocate when there are entries; an empty list would alloc 0 bytes.
|
||||
;; The wasm side reads an empty buffer as zero rows.
|
||||
(when (seq entries)
|
||||
(let [size (mem/get-alloc-size entries GRID-LAYOUT-ROW-U8-SIZE)
|
||||
offset (mem/alloc size)
|
||||
dview (mem/get-data-view)]
|
||||
|
||||
(reduce (fn [offset {:keys [type value]}]
|
||||
(-> offset
|
||||
(mem/write-u8 dview (sr/translate-grid-track-type type))
|
||||
(+ 3) ;; padding
|
||||
(mem/write-f32 dview value)
|
||||
(mem/assert-written offset GRID-LAYOUT-ROW-U8-SIZE)))
|
||||
(reduce (fn [offset {:keys [type value]}]
|
||||
(-> offset
|
||||
(mem/write-u8 dview (sr/translate-grid-track-type type))
|
||||
(+ 3) ;; padding
|
||||
(mem/write-f32 dview value)
|
||||
(mem/assert-written offset GRID-LAYOUT-ROW-U8-SIZE)))
|
||||
|
||||
offset
|
||||
entries)
|
||||
offset
|
||||
entries)))
|
||||
|
||||
(h/call wasm/internal-module "_set_grid_rows")))
|
||||
(h/call wasm/internal-module "_set_grid_rows"))
|
||||
|
||||
(defn set-grid-layout-columns
|
||||
[entries]
|
||||
(let [size (mem/get-alloc-size entries GRID-LAYOUT-COLUMN-U8-SIZE)
|
||||
offset (mem/alloc size)
|
||||
dview (mem/get-data-view)]
|
||||
;; Only allocate when there are entries; an empty list would alloc 0 bytes.
|
||||
;; The wasm side reads an empty buffer as zero columns.
|
||||
(when (seq entries)
|
||||
(let [size (mem/get-alloc-size entries GRID-LAYOUT-COLUMN-U8-SIZE)
|
||||
offset (mem/alloc size)
|
||||
dview (mem/get-data-view)]
|
||||
|
||||
(reduce (fn [offset {:keys [type value]}]
|
||||
(-> offset
|
||||
(mem/write-u8 dview (sr/translate-grid-track-type type))
|
||||
(+ 3) ;; padding
|
||||
(mem/write-f32 dview value)
|
||||
(mem/assert-written offset GRID-LAYOUT-COLUMN-U8-SIZE)))
|
||||
offset
|
||||
entries)
|
||||
(reduce (fn [offset {:keys [type value]}]
|
||||
(-> offset
|
||||
(mem/write-u8 dview (sr/translate-grid-track-type type))
|
||||
(+ 3) ;; padding
|
||||
(mem/write-f32 dview value)
|
||||
(mem/assert-written offset GRID-LAYOUT-COLUMN-U8-SIZE)))
|
||||
offset
|
||||
entries)))
|
||||
|
||||
(h/call wasm/internal-module "_set_grid_columns")))
|
||||
(h/call wasm/internal-module "_set_grid_columns"))
|
||||
|
||||
(defn set-grid-layout-cells
|
||||
[cells]
|
||||
(let [size (mem/get-alloc-size cells GRID-LAYOUT-CELL-U8-SIZE)
|
||||
offset (mem/alloc size)
|
||||
dview (mem/get-data-view)]
|
||||
;; Only allocate when there are cells; an empty collection would alloc 0
|
||||
;; bytes. The wasm side reads an empty buffer as zero cells.
|
||||
(when (seq cells)
|
||||
(let [size (mem/get-alloc-size cells GRID-LAYOUT-CELL-U8-SIZE)
|
||||
offset (mem/alloc size)
|
||||
dview (mem/get-data-view)]
|
||||
|
||||
(reduce-kv (fn [offset _ cell]
|
||||
(let [shape-id (-> (get cell :shapes) first)]
|
||||
(-> offset
|
||||
(mem/write-i32 dview (get cell :row))
|
||||
(mem/write-i32 dview (get cell :row-span))
|
||||
(mem/write-i32 dview (get cell :column))
|
||||
(mem/write-i32 dview (get cell :column-span))
|
||||
(reduce-kv (fn [offset _ cell]
|
||||
(let [shape-id (-> (get cell :shapes) first)]
|
||||
(-> offset
|
||||
(mem/write-i32 dview (get cell :row))
|
||||
(mem/write-i32 dview (get cell :row-span))
|
||||
(mem/write-i32 dview (get cell :column))
|
||||
(mem/write-i32 dview (get cell :column-span))
|
||||
|
||||
(mem/write-u8 dview (sr/translate-align-self (get cell :align-self)))
|
||||
(mem/write-u8 dview (sr/translate-justify-self (get cell :justify-self)))
|
||||
(mem/write-u8 dview (sr/translate-align-self (get cell :align-self)))
|
||||
(mem/write-u8 dview (sr/translate-justify-self (get cell :justify-self)))
|
||||
|
||||
;; padding
|
||||
(+ 2)
|
||||
;; padding
|
||||
(+ 2)
|
||||
|
||||
(mem/write-uuid dview (d/nilv shape-id uuid/zero))
|
||||
(mem/assert-written offset GRID-LAYOUT-CELL-U8-SIZE))))
|
||||
(mem/write-uuid dview (d/nilv shape-id uuid/zero))
|
||||
(mem/assert-written offset GRID-LAYOUT-CELL-U8-SIZE))))
|
||||
|
||||
offset
|
||||
cells)
|
||||
offset
|
||||
cells)))
|
||||
|
||||
(h/call wasm/internal-module "_set_grid_cells")))
|
||||
(h/call wasm/internal-module "_set_grid_cells"))
|
||||
|
||||
(defn set-grid-layout
|
||||
[shape]
|
||||
|
||||
@ -126,6 +126,39 @@
|
||||
[_ms]
|
||||
(rx/of :immediate))
|
||||
|
||||
;; Static-dispatch-safe stubs
|
||||
;; ═══════════════════════════════════════════════════════════════
|
||||
;;
|
||||
;; The `:esm` test build compiles calls to a *multi-arity* var as
|
||||
;; `f.cljs$core$IFn$_invoke$arity$N(...)`. A plain single-arity `fn`
|
||||
;; (including `identity`) does not expose that property, so using one
|
||||
;; to redefine such a var throws "arity$N is not a function". Multi-arity
|
||||
;; fns do expose the property, hence the helpers below.
|
||||
|
||||
(defn noop
|
||||
"Multi-arity no-op. Use to stub static-dispatched multi-arity vars
|
||||
such as `st/emit!` (replacing `identity`, which is single-arity)."
|
||||
([] nil)
|
||||
([_] nil)
|
||||
([_ _] nil)
|
||||
([_ _ _] nil)
|
||||
([_ _ _ _] nil)
|
||||
([_ _ _ _ & _] nil))
|
||||
|
||||
(defn stub
|
||||
"Wraps `f` in a multi-arity fn (arities 0-6) delegating to `f`, so the
|
||||
result exposes `cljs$core$IFn$_invoke$arity$N`. Required when replacing
|
||||
a multi-arity var in a `with-redefs`/`set!` mock with a capturing fn."
|
||||
[f]
|
||||
(fn
|
||||
([] (f))
|
||||
([a] (f a))
|
||||
([a b] (f a b))
|
||||
([a b c] (f a b c))
|
||||
([a b c d] (f a b c d))
|
||||
([a b c d e] (f a b c d e))
|
||||
([a b c d e g] (f a b c d e g))))
|
||||
|
||||
;; Lifecycle
|
||||
;; ═══════════════════════════════════════════════════════════════
|
||||
|
||||
|
||||
60
frontend/test/frontend_tests/plugins/comments_test.cljs
Normal file
60
frontend/test/frontend_tests/plugins/comments_test.cljs
Normal file
@ -0,0 +1,60 @@
|
||||
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
;;
|
||||
;; Copyright (c) KALEIDOS INC Sucursal en España SL
|
||||
|
||||
(ns frontend-tests.plugins.comments-test
|
||||
(:require
|
||||
[app.main.data.comments :as dc]
|
||||
[app.main.store :as st]
|
||||
[app.plugins.comments :as comments]
|
||||
[app.plugins.page :as page]
|
||||
[app.plugins.register :as r]
|
||||
[cljs.test :as t :include-macros true]
|
||||
[frontend-tests.helpers.mock :as mock]))
|
||||
|
||||
(def ^:private plugin-id "00000000-0000-0000-0000-000000000000")
|
||||
|
||||
(t/deftest comment-thread-remove-allows-the-owner
|
||||
(let [owner-id (random-uuid)
|
||||
file-id (random-uuid)
|
||||
page-id (random-uuid)
|
||||
thread-id (random-uuid)
|
||||
emitted (atom nil)
|
||||
thread (comments/comment-thread-proxy
|
||||
plugin-id
|
||||
file-id
|
||||
page-id
|
||||
{:id thread-id :owner-id owner-id})]
|
||||
(set! st/state (atom {:profile {:id owner-id}}))
|
||||
(with-redefs [r/check-permission (constantly true)
|
||||
dc/delete-comment-thread-on-workspace
|
||||
(mock/stub (fn [params callback]
|
||||
(callback)
|
||||
[:delete-thread params]))
|
||||
st/emit! (mock/stub (fn [event] (reset! emitted event)))]
|
||||
(let [result (.remove thread)]
|
||||
(t/is (instance? js/Promise result))
|
||||
(t/is (= [:delete-thread {:id thread-id}] @emitted))))))
|
||||
|
||||
(t/deftest page-remove-comment-thread-emits-delete-event
|
||||
(let [file-id (random-uuid)
|
||||
page-id (random-uuid)
|
||||
thread-id (random-uuid)
|
||||
emitted (atom nil)
|
||||
page (page/page-proxy plugin-id file-id page-id)
|
||||
thread (comments/comment-thread-proxy
|
||||
plugin-id
|
||||
file-id
|
||||
page-id
|
||||
{:id thread-id :owner-id (random-uuid)})]
|
||||
(with-redefs [r/check-permission (constantly true)
|
||||
dc/delete-comment-thread-on-workspace
|
||||
(mock/stub (fn [params callback]
|
||||
(callback)
|
||||
[:delete-thread params]))
|
||||
st/emit! (mock/stub (fn [event] (reset! emitted event)))]
|
||||
(let [result (.removeCommentThread page thread)]
|
||||
(t/is (instance? js/Promise result))
|
||||
(t/is (= [:delete-thread {:id thread-id}] @emitted))))))
|
||||
21
frontend/test/frontend_tests/plugins/file_test.cljs
Normal file
21
frontend/test/frontend_tests/plugins/file_test.cljs
Normal file
@ -0,0 +1,21 @@
|
||||
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
;;
|
||||
;; Copyright (c) KALEIDOS INC Sucursal en España SL
|
||||
|
||||
(ns frontend-tests.plugins.file-test
|
||||
(:require
|
||||
[app.plugins.file :as file]
|
||||
[cljs.test :as t :include-macros true]))
|
||||
|
||||
(t/deftest file-version-created-at-returns-stored-date
|
||||
(let [created-at (js/Date.)
|
||||
version (file/file-version-proxy
|
||||
"00000000-0000-0000-0000-000000000000"
|
||||
(random-uuid)
|
||||
{}
|
||||
{:id (random-uuid)
|
||||
:label "Version"
|
||||
:created-at created-at})]
|
||||
(t/is (identical? created-at (.-createdAt version)))))
|
||||
@ -38,3 +38,17 @@
|
||||
(format/format-frame-guides nil)
|
||||
(format/format-tracks nil)
|
||||
(format/format-path-content nil)))
|
||||
|
||||
(t/deftest test-format-color-result-uses-shapes-info-key
|
||||
(let [shape-id (random-uuid)
|
||||
result (format/format-color-result
|
||||
[{:color "#fabada"}
|
||||
[{:prop :fill :shape-id shape-id :index 0}]])
|
||||
info (aget result "shapesInfo")]
|
||||
(t/is (array? info))
|
||||
(t/is (nil? (aget result "shapesColors")))
|
||||
(t/is (= "fill" (aget (aget info 0) "property")))
|
||||
(t/is (= (str shape-id) (aget (aget info 0) "shapeId")))))
|
||||
|
||||
(t/deftest test-shape-type-reports-boolean
|
||||
(t/is (= "boolean" (format/shape-type :bool))))
|
||||
|
||||
49
frontend/test/frontend_tests/plugins/grid_test.cljs
Normal file
49
frontend/test/frontend_tests/plugins/grid_test.cljs
Normal file
@ -0,0 +1,49 @@
|
||||
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
;;
|
||||
;; Copyright (c) KALEIDOS INC Sucursal en España SL
|
||||
|
||||
(ns frontend-tests.plugins.grid-test
|
||||
(:require
|
||||
[app.common.test-helpers.files :as cthf]
|
||||
[app.main.store :as st]
|
||||
[app.plugins.api :as api]
|
||||
[cljs.test :as t :include-macros true]
|
||||
[frontend-tests.helpers.state :as ths]
|
||||
[frontend-tests.helpers.wasm :as thw]
|
||||
[potok.v2.core :as ptk]))
|
||||
|
||||
(def ^:private plugin-id "00000000-0000-0000-0000-000000000000")
|
||||
|
||||
(defn- setup-grid []
|
||||
(let [store (ths/setup-store (cthf/sample-file :file1 :page-label :page1))
|
||||
_ (set! st/state store)
|
||||
_ (set! st/stream (ptk/input-stream store))
|
||||
context (api/create-context plugin-id)
|
||||
board (.createBoard ^js context)
|
||||
grid (.addGridLayout ^js board)]
|
||||
{:store store :context context :board board :grid grid}))
|
||||
|
||||
(t/deftest add-column-at-index-accepts-fixed-track-type
|
||||
(thw/with-wasm-mocks*
|
||||
(fn []
|
||||
(let [{:keys [^js grid]} (setup-grid)]
|
||||
(.addColumn grid "flex" 1)
|
||||
(.addColumnAtIndex grid 0 "fixed" 100)
|
||||
(t/is (= "fixed" (aget (aget (.-columns grid) 0) "type")))
|
||||
(t/is (= 100 (aget (aget (.-columns grid) 0) "value")))))))
|
||||
|
||||
(t/deftest grid-track-methods-reject-out-of-range-indices
|
||||
(thw/with-wasm-mocks*
|
||||
(fn []
|
||||
(let [{:keys [store ^js grid]} (setup-grid)]
|
||||
(swap! store assoc-in [:plugins :flags plugin-id :throw-validation-errors] true)
|
||||
(.addRow grid "flex" 1)
|
||||
(.addColumn grid "flex" 1)
|
||||
(t/is (thrown? js/Error (.addRowAtIndex grid -1 "fixed" 10)))
|
||||
(t/is (thrown? js/Error (.addColumnAtIndex grid 2 "fixed" 10)))
|
||||
(t/is (thrown? js/Error (.setRow grid 1 "fixed" 10)))
|
||||
(t/is (thrown? js/Error (.setColumn grid 1 "fixed" 10)))
|
||||
(t/is (thrown? js/Error (.removeRow grid 1)))
|
||||
(t/is (thrown? js/Error (.removeColumn grid 1)))))))
|
||||
95
frontend/test/frontend_tests/plugins/library_test.cljs
Normal file
95
frontend/test/frontend_tests/plugins/library_test.cljs
Normal file
@ -0,0 +1,95 @@
|
||||
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
;;
|
||||
;; Copyright (c) KALEIDOS INC Sucursal en España SL
|
||||
|
||||
(ns frontend-tests.plugins.library-test
|
||||
(:require
|
||||
[app.main.data.workspace.libraries :as dwl]
|
||||
[app.main.data.workspace.texts :as dwt]
|
||||
[app.main.store :as st]
|
||||
[app.plugins.library :as library]
|
||||
[app.plugins.register :as r]
|
||||
[app.plugins.text :as text]
|
||||
[app.plugins.utils :as u]
|
||||
[cljs.test :as t :include-macros true]
|
||||
[frontend-tests.helpers.mock :as mock]))
|
||||
|
||||
(def ^:private plugin-id "00000000-0000-0000-0000-000000000000")
|
||||
|
||||
(t/deftest library-asset-proxies-expose-library-id
|
||||
(let [file-id (random-uuid)
|
||||
id (random-uuid)]
|
||||
(t/is (= (str file-id) (.-libraryId (library/lib-color-proxy plugin-id file-id id))))
|
||||
(t/is (= (str file-id) (.-libraryId (library/lib-typography-proxy plugin-id file-id id))))
|
||||
(t/is (= (str file-id) (.-libraryId (library/lib-component-proxy plugin-id file-id id))))))
|
||||
|
||||
(t/deftest typography-apply-to-text-range-uses-hidden-range-bounds
|
||||
(let [file-id (random-uuid)
|
||||
page-id (random-uuid)
|
||||
shape-id (random-uuid)
|
||||
typography-id (random-uuid)
|
||||
typography (library/lib-typography-proxy plugin-id file-id typography-id)
|
||||
text-range (text/text-range-proxy plugin-id file-id page-id shape-id 2 5)
|
||||
captured (atom nil)]
|
||||
(with-redefs [r/check-permission (constantly true)
|
||||
u/page-active? (constantly true)
|
||||
u/locate-library-typography
|
||||
(constantly {:id typography-id
|
||||
:name "Body"
|
||||
:font-size "14"})
|
||||
dwt/update-text-range
|
||||
(fn [shape-id start end attrs]
|
||||
(reset! captured {:shape-id shape-id
|
||||
:start start
|
||||
:end end
|
||||
:attrs attrs})
|
||||
:update-text-range)
|
||||
st/emit! mock/noop]
|
||||
(.applyToTextRange typography text-range)
|
||||
(t/is (= shape-id (:shape-id @captured)))
|
||||
(t/is (= 2 (:start @captured)))
|
||||
(t/is (= 5 (:end @captured)))
|
||||
(t/is (= file-id (get-in @captured [:attrs :typography-ref-file])))
|
||||
(t/is (= typography-id (get-in @captured [:attrs :typography-ref-id]))))))
|
||||
|
||||
(t/deftest library-color-gradient-and-image-clear-exclusive-representations
|
||||
(let [file-id (random-uuid)
|
||||
color-id (random-uuid)
|
||||
proxy (library/lib-color-proxy plugin-id file-id color-id)
|
||||
captured (atom nil)
|
||||
base {:id color-id
|
||||
:name "Brand"
|
||||
:color "#fabada"
|
||||
:opacity 1
|
||||
:gradient {:type :linear}
|
||||
:image {:id (random-uuid) :width 1 :height 1}}]
|
||||
(with-redefs [r/check-permission (constantly true)
|
||||
u/proxy->library-color (constantly base)
|
||||
dwl/update-color-data (fn [color file-id]
|
||||
(reset! captured {:color color :file-id file-id})
|
||||
:update-color-data)
|
||||
st/emit! mock/noop]
|
||||
(set! (.-gradient proxy)
|
||||
#js {:type "linear"
|
||||
:startX 0
|
||||
:startY 0
|
||||
:endX 1
|
||||
:endY 1
|
||||
:width 1
|
||||
:stops #js [#js {:color "#000000"
|
||||
:opacity 1
|
||||
:offset 0}]})
|
||||
(t/is (contains? (:color @captured) :gradient))
|
||||
(t/is (not (contains? (:color @captured) :color)))
|
||||
(t/is (not (contains? (:color @captured) :image)))
|
||||
|
||||
(set! (.-image proxy)
|
||||
#js {:id (str (random-uuid))
|
||||
:width 10
|
||||
:height 20
|
||||
:mtype "image/png"})
|
||||
(t/is (contains? (:color @captured) :image))
|
||||
(t/is (not (contains? (:color @captured) :color)))
|
||||
(t/is (not (contains? (:color @captured) :gradient))))))
|
||||
28
frontend/test/frontend_tests/plugins/local_storage_test.cljs
Normal file
28
frontend/test/frontend_tests/plugins/local_storage_test.cljs
Normal file
@ -0,0 +1,28 @@
|
||||
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
;;
|
||||
;; Copyright (c) KALEIDOS INC Sucursal en España SL
|
||||
|
||||
(ns frontend-tests.plugins.local-storage-test
|
||||
(:require
|
||||
[app.plugins.local-storage :as storage]
|
||||
[app.plugins.register :as r]
|
||||
[cljs.test :as t :include-macros true]))
|
||||
|
||||
(t/deftest remove-item-removes-the-prefixed-key
|
||||
(let [data (atom {})
|
||||
fake #js {}
|
||||
plugin-id "plugin-a"]
|
||||
(set! (.-getItem fake) (fn [key] (get @data key)))
|
||||
(set! (.-setItem fake) (fn [key value] (swap! data assoc key value)))
|
||||
(set! (.-removeItem fake) (fn [key] (swap! data dissoc key)))
|
||||
(set! (.-keys fake) (fn [] (to-array (keys @data))))
|
||||
(with-redefs [r/check-permission (constantly true)
|
||||
storage/local-storage fake]
|
||||
(let [proxy (storage/local-storage-proxy plugin-id)]
|
||||
(.setItem proxy "key" "value")
|
||||
(t/is (= "value" (.getItem proxy "key")))
|
||||
(.removeItem proxy "key")
|
||||
(t/is (nil? (.getItem proxy "key")))
|
||||
(t/is (empty? @data))))))
|
||||
@ -31,3 +31,33 @@
|
||||
(t/is (gpt/point? result))
|
||||
(t/is (= 0 (:x result)))
|
||||
(t/is (= 0 (:y result))))))
|
||||
|
||||
(t/deftest test-parse-overlay-action-defaults-manual-position
|
||||
(let [destination #js {"$id" (random-uuid)}
|
||||
action (parser/parse-action
|
||||
#js {:type "open-overlay"
|
||||
:destination destination
|
||||
:position "center"})]
|
||||
(t/is (= :open-overlay (:action-type action)))
|
||||
(t/is (= :center (:overlay-pos-type action)))
|
||||
(t/is (gpt/point? (:overlay-position action)))
|
||||
(t/is (= 0 (:x (:overlay-position action))))
|
||||
(t/is (= 0 (:y (:overlay-position action))))))
|
||||
|
||||
(t/deftest test-parse-frame-guide-calls-guide-parser
|
||||
(let [column (parser/parse-frame-guide
|
||||
#js {:type "column"
|
||||
:display true
|
||||
:params #js {:type "stretch"
|
||||
:size 12}})
|
||||
row (parser/parse-frame-guide
|
||||
#js {:type "row"
|
||||
:display false
|
||||
:params #js {:type "center"
|
||||
:margin 4}})]
|
||||
(t/is (= :column (:type column)))
|
||||
(t/is (= true (:display column)))
|
||||
(t/is (= :stretch (get-in column [:params :type])))
|
||||
(t/is (= :row (:type row)))
|
||||
(t/is (= false (:display row)))
|
||||
(t/is (= :center (get-in row [:params :type])))))
|
||||
|
||||
202
frontend/test/frontend_tests/plugins/shape_bugfixes_test.cljs
Normal file
202
frontend/test/frontend_tests/plugins/shape_bugfixes_test.cljs
Normal file
@ -0,0 +1,202 @@
|
||||
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
;;
|
||||
;; Copyright (c) KALEIDOS INC Sucursal en España SL
|
||||
|
||||
(ns frontend-tests.plugins.shape-bugfixes-test
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.test-helpers.files :as cthf]
|
||||
[app.common.types.component :as ctk]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.main.data.workspace :as dw]
|
||||
[app.main.data.workspace.variants :as dwv]
|
||||
[app.main.store :as st]
|
||||
[app.plugins.api :as api]
|
||||
[app.plugins.public-utils :as public-utils]
|
||||
[app.plugins.shape :as shape]
|
||||
[app.plugins.utils :as u]
|
||||
[cljs.test :as t :include-macros true]
|
||||
[frontend-tests.helpers.mock :as mock]
|
||||
[frontend-tests.helpers.state :as ths]
|
||||
[frontend-tests.helpers.wasm :as thw]))
|
||||
|
||||
(def ^:private plugin-id "00000000-0000-0000-0000-000000000000")
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Helpers
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defn- child-shapes
|
||||
"Ordered child shape ids of `board`, read back from the live store
|
||||
(the observable result of a z-order operation)."
|
||||
[store ^js context ^js board]
|
||||
(let [file-id (aget (. context -currentFile) "$id")
|
||||
page-id (aget (. context -currentPage) "$id")
|
||||
board-id (aget board "$id")]
|
||||
(get-in @store [:files file-id :data :pages-index page-id
|
||||
:objects board-id :shapes])))
|
||||
|
||||
(defn- page-guides
|
||||
"The guides map of the current page, read back from the live store."
|
||||
[store ^js context]
|
||||
(let [file-id (aget (. context -currentFile) "$id")
|
||||
page-id (aget (. context -currentPage) "$id")]
|
||||
(get-in @store [:files file-id :data :pages-index page-id :guides])))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Tests
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(t/deftest trigger-setter-updates-the-interaction-event-type
|
||||
;; Regression: the `trigger` setter must update the interaction of the
|
||||
;; located shape. Asserting on the observable interaction (read back through
|
||||
;; the proxy from the live store) covers that without coupling to which
|
||||
;; internal action gets emitted.
|
||||
(thw/with-wasm-mocks*
|
||||
(fn []
|
||||
(let [store (ths/setup-store (cthf/sample-file :file1 :page-label :page1))
|
||||
^js context (api/create-context plugin-id)
|
||||
_ (set! st/state store)
|
||||
^js board (.createBoard context)]
|
||||
(.addInteraction board "click" #js {:type "open-url" :url "https://example.com"})
|
||||
(let [^js interaction (aget (.-interactions board) 0)]
|
||||
(t/is (= "click" (.-trigger interaction))
|
||||
"the interaction starts with the click trigger")
|
||||
(set! (.-trigger interaction) "mouse-over")
|
||||
(t/is (= "mouse-over" (.-trigger interaction))
|
||||
"the trigger setter updates the interaction event-type"))))))
|
||||
|
||||
(t/deftest center-shapes-empty-input-returns-nil
|
||||
(t/is (nil? (public-utils/centerShapes #js []))))
|
||||
|
||||
(t/deftest background-blur-reads-background-blur-key
|
||||
(let [file-id (uuid/next)
|
||||
page-id (uuid/next)
|
||||
shape-id (uuid/next)
|
||||
blur-id (uuid/next)
|
||||
proxy (shape/shape-proxy plugin-id file-id page-id shape-id)]
|
||||
(with-redefs [u/proxy->shape (constantly {:background-blur {:id blur-id
|
||||
:value 12
|
||||
:hidden false}})]
|
||||
(let [blur (.-backgroundBlur proxy)]
|
||||
(t/is (= (str blur-id) (aget blur "id")))
|
||||
(t/is (= 12 (aget blur "value")))))))
|
||||
|
||||
(t/deftest flatten-returns-proxies-for-converted-shapes
|
||||
;; `convert-selected-to-path` runs the WASM boolean/path pipeline, so this
|
||||
;; test stays at the proxy boundary: it verifies `flatten` forwards the
|
||||
;; selected ids to the conversion and wraps the result back into proxies.
|
||||
(let [file-id (uuid/next)
|
||||
page-id (uuid/next)
|
||||
shape-id (uuid/next)
|
||||
input (shape/shape-proxy plugin-id file-id page-id shape-id)
|
||||
emitted (atom nil)
|
||||
context (api/create-context plugin-id)]
|
||||
(set! st/state (atom {:current-file-id file-id
|
||||
:current-page-id page-id}))
|
||||
(with-redefs [dw/convert-selected-to-path
|
||||
(mock/stub (fn [ids]
|
||||
(reset! emitted ids)
|
||||
:convert-selected-to-path))
|
||||
st/emit! mock/noop
|
||||
shape/shape-proxy
|
||||
(mock/stub (fn [_plugin file page id]
|
||||
#js {"$file" file "$page" page "$id" id}))]
|
||||
(let [result (.flatten context #js [input])]
|
||||
(t/is (= #{shape-id} @emitted))
|
||||
(t/is (array? result))
|
||||
(t/is (= shape-id (aget result 0 "$id")))
|
||||
(t/is (= file-id (aget result 0 "$file")))
|
||||
(t/is (= page-id (aget result 0 "$page")))))))
|
||||
|
||||
(t/deftest z-order-methods-reorder-the-shape-within-its-parent
|
||||
;; Asserts the observable child order in the parent after each z-order
|
||||
;; method, instead of merely checking which location keyword was emitted.
|
||||
;; The assertions are independent of the parent's `:shapes` ordering
|
||||
;; convention: a reorder is verified by relative movement and extremes.
|
||||
(thw/with-wasm-mocks*
|
||||
(fn []
|
||||
(let [store (ths/setup-store (cthf/sample-file :file1 :page-label :page1))
|
||||
^js context (api/create-context plugin-id)
|
||||
_ (set! st/state store)
|
||||
^js board (.createBoard context)
|
||||
children (mapv (fn [_] (.createRectangle context)) (range 4))
|
||||
ids (mapv #(aget % "$id") children)
|
||||
order #(child-shapes store context board)]
|
||||
(doseq [^js c children] (.appendChild board c))
|
||||
|
||||
;; Operate on a shape that is currently interior (so both a forward
|
||||
;; and a backward step are observable).
|
||||
(let [mid-id (nth (order) 1)
|
||||
^js mid (nth children (d/index-of ids mid-id))]
|
||||
|
||||
(t/testing "bringForward and sendBackward move in opposite directions"
|
||||
(let [i0 (d/index-of (order) mid-id)
|
||||
_ (.bringForward mid)
|
||||
i1 (d/index-of (order) mid-id)
|
||||
_ (.sendBackward mid)
|
||||
i2 (d/index-of (order) mid-id)]
|
||||
(t/is (not= i0 i1) "bringForward changes the order")
|
||||
(t/is (not= i1 i2) "sendBackward changes the order")
|
||||
(t/is (= (pos? (- i1 i0)) (neg? (- i2 i1)))
|
||||
"the two steps move the shape in opposite directions")))
|
||||
|
||||
(t/testing "bringToFront and sendToBack move to opposite extremes"
|
||||
(let [n (count (order))
|
||||
_ (.bringToFront mid)
|
||||
p1 (d/index-of (order) mid-id)
|
||||
_ (.sendToBack mid)
|
||||
p2 (d/index-of (order) mid-id)]
|
||||
(t/is (contains? #{0 (dec n)} p1) "bringToFront moves to an extreme")
|
||||
(t/is (contains? #{0 (dec n)} p2) "sendToBack moves to an extreme")
|
||||
(t/is (not= p1 p2) "front and back are opposite extremes"))))))))
|
||||
|
||||
(t/deftest is-variant-container-predicate-returns-boolean
|
||||
(t/is (false? (ctk/is-variant-container? {})))
|
||||
(t/is (true? (ctk/is-variant-container? {:is-variant-container true}))))
|
||||
|
||||
(t/deftest combine-as-variants-uses-the-passed-component-ids
|
||||
;; `combine-as-variants` needs real main components and the variant pipeline,
|
||||
;; so this stays at the proxy boundary and verifies the component ids that
|
||||
;; the head proxy collects from its argument before delegating.
|
||||
(let [file-id (uuid/next)
|
||||
page-id (uuid/next)
|
||||
head-id (uuid/next)
|
||||
other-id (uuid/next)
|
||||
proxy (shape/shape-proxy plugin-id file-id page-id head-id)
|
||||
captured (atom nil)]
|
||||
(with-redefs [u/locate-shape (fn [_file _page id] {:id id :component-id id})
|
||||
u/locate-library-component (constantly {:id (uuid/next)})
|
||||
ctk/is-variant? (constantly false)
|
||||
dwv/combine-as-variants
|
||||
(fn [ids opts]
|
||||
(reset! captured {:ids ids :opts opts})
|
||||
;; return value flows through `se/add-event` (which
|
||||
;; calls `with-meta`), so it must support metadata
|
||||
{:event :combine-as-variants})
|
||||
st/emit! mock/noop
|
||||
shape/shape-proxy (mock/stub (fn [& _] #js {}))]
|
||||
(.combineAsVariants proxy #js [(str other-id)])
|
||||
(t/is (= #{head-id other-id} (:ids @captured))))))
|
||||
|
||||
(t/deftest remove-ruler-guide-deletes-the-guide-from-the-page
|
||||
;; Adds a real ruler guide through the API and asserts it is gone from the
|
||||
;; page guides after removeRulerGuide, rather than checking the removal call.
|
||||
(thw/with-wasm-mocks*
|
||||
(fn []
|
||||
(let [store (ths/setup-store (cthf/sample-file :file1 :page-label :page1))
|
||||
^js context (api/create-context plugin-id)
|
||||
_ (set! st/state store)
|
||||
^js board (.createBoard context)
|
||||
^js guide (.addRulerGuide board "horizontal" 10)]
|
||||
(t/is (= 1 (count (page-guides store context)))
|
||||
"addRulerGuide stores one guide on the page")
|
||||
(.removeRulerGuide board guide)
|
||||
(t/is (empty? (page-guides store context))
|
||||
"removeRulerGuide deletes the guide from the page")))))
|
||||
|
||||
(t/deftest group-empty-input-returns-nil
|
||||
(let [context (api/create-context plugin-id)]
|
||||
(t/is (nil? (.group context #js [])))))
|
||||
87
frontend/test/frontend_tests/plugins/text_test.cljs
Normal file
87
frontend/test/frontend_tests/plugins/text_test.cljs
Normal file
@ -0,0 +1,87 @@
|
||||
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
;;
|
||||
;; Copyright (c) KALEIDOS INC
|
||||
|
||||
(ns frontend-tests.plugins.text-test
|
||||
(:require
|
||||
[app.main.data.workspace.texts :as dwt]
|
||||
[app.main.store :as st]
|
||||
[app.plugins.fonts :as fonts]
|
||||
[app.plugins.format :as format]
|
||||
[app.plugins.register :as r]
|
||||
[app.plugins.shape :as shape]
|
||||
[app.plugins.text :as plugins.text]
|
||||
[app.plugins.utils :as u]
|
||||
[cljs.test :as t :include-macros true]
|
||||
[frontend-tests.helpers.mock :as mock]))
|
||||
|
||||
(def ^:private plugin-id "00000000-0000-0000-0000-000000000000")
|
||||
|
||||
(t/deftest font-apply-to-text-uses-font-id-not-shape-id
|
||||
(let [file-id (random-uuid)
|
||||
page-id (random-uuid)
|
||||
shape-id (random-uuid)
|
||||
font (fonts/font-proxy
|
||||
plugin-id
|
||||
{:id "font-id"
|
||||
:family "Inter"
|
||||
:name "Inter"
|
||||
:variants [{:id "regular"
|
||||
:name "Regular"
|
||||
:weight "400"
|
||||
:style "normal"}]})
|
||||
text (shape/shape-proxy plugin-id file-id page-id shape-id)
|
||||
captured (atom nil)]
|
||||
(with-redefs [r/check-permission (constantly true)
|
||||
u/page-active? (constantly true)
|
||||
dwt/update-attrs
|
||||
(fn [id attrs]
|
||||
(reset! captured {:id id :attrs attrs})
|
||||
:update-attrs)
|
||||
st/emit! mock/noop]
|
||||
(.applyToText font text nil)
|
||||
(t/is (= shape-id (:id @captured)))
|
||||
(t/is (= "font-id" (get-in @captured [:attrs :font-id]))))))
|
||||
|
||||
(t/deftest font-apply-to-range-uses-hidden-range-bounds
|
||||
(let [file-id (random-uuid)
|
||||
page-id (random-uuid)
|
||||
shape-id (random-uuid)
|
||||
font (fonts/font-proxy
|
||||
plugin-id
|
||||
{:id "font-id"
|
||||
:family "Inter"
|
||||
:name "Inter"
|
||||
:variants [{:id "regular"
|
||||
:name "Regular"
|
||||
:weight "400"
|
||||
:style "normal"}]})
|
||||
range (plugins.text/text-range-proxy plugin-id file-id page-id shape-id 1 4)
|
||||
captured (atom nil)]
|
||||
(with-redefs [r/check-permission (constantly true)
|
||||
u/page-active? (constantly true)
|
||||
dwt/update-text-range
|
||||
(fn [id start end attrs]
|
||||
(reset! captured {:id id
|
||||
:start start
|
||||
:end end
|
||||
:attrs attrs})
|
||||
:update-text-range)
|
||||
st/emit! mock/noop]
|
||||
(.applyToRange font range nil)
|
||||
(t/is (= shape-id (:id @captured)))
|
||||
(t/is (= 1 (:start @captured)))
|
||||
(t/is (= 4 (:end @captured)))
|
||||
(t/is (= "font-id" (get-in @captured [:attrs :font-id]))))))
|
||||
|
||||
(t/deftest text-range-shape-returns-a-shape-proxy
|
||||
(let [file-id (random-uuid)
|
||||
page-id (random-uuid)
|
||||
shape-id (random-uuid)
|
||||
range (plugins.text/text-range-proxy plugin-id file-id page-id shape-id 0 3)]
|
||||
(with-redefs [format/shape-proxy shape/shape-proxy]
|
||||
(let [text-shape (.-shape range)]
|
||||
(t/is (shape/shape-proxy? text-shape))
|
||||
(t/is (= shape-id (aget text-shape "$id")))))))
|
||||
@ -12,15 +12,20 @@
|
||||
[app.common.test-helpers.tokens :as ctht]
|
||||
[app.common.types.tokens-lib :as ctob]
|
||||
[app.main.data.tokenscript :as ts]
|
||||
[app.main.data.workspace.tokens.library-edit :as dwtl]
|
||||
[app.main.store :as st]
|
||||
[app.plugins.api :as api]
|
||||
[app.plugins.tokens :as ptok]
|
||||
[app.plugins.utils :as u]
|
||||
[cljs.test :as t :include-macros true]
|
||||
[frontend-tests.helpers.mock :as mock]
|
||||
[frontend-tests.helpers.state :as ths]
|
||||
[potok.v2.core :as ptk]))
|
||||
|
||||
(t/use-fixtures :each {:before cthi/reset-idmap!})
|
||||
|
||||
(def ^:private get-resolved-value @#'ptok/get-resolved-value)
|
||||
|
||||
;; Regression coverage for issue #9162.
|
||||
;;
|
||||
;; Plugin code calling `shape.applyToken(token, ["fill"])` or
|
||||
@ -226,3 +231,110 @@
|
||||
{:keys [errors resolved-value]} (get resolved (:name token))]
|
||||
(t/is (nil? resolved-value))
|
||||
(t/is (seq errors))))
|
||||
|
||||
(t/deftest token-set-duplicate-returns-the-duplicated-set
|
||||
(let [file-id (cthi/new-id! :file)
|
||||
set-id (cthi/new-id! :set)
|
||||
dup-id (cthi/new-id! :dup)
|
||||
proxy (ptok/token-set-proxy "plugin-id" file-id set-id)]
|
||||
(with-redefs [dwtl/duplicate-token-set
|
||||
(mock/stub (fn [id {:keys [id-ref]}]
|
||||
(t/is (= set-id id))
|
||||
(reset! id-ref dup-id)
|
||||
:duplicate-token-set))
|
||||
st/emit! mock/noop]
|
||||
(let [dup (.duplicate proxy)]
|
||||
(t/is (ptok/token-set-proxy? dup))
|
||||
(t/is (= (str dup-id) (.-id dup)))))))
|
||||
|
||||
(t/deftest theme-add-set-and-remove-set-use-the-set-name
|
||||
(let [file-id (cthi/new-id! :file)
|
||||
theme-id (cthi/new-id! :theme)
|
||||
set-id (cthi/new-id! :set)
|
||||
set (ptok/token-set-proxy "plugin-id" file-id set-id "Primitives")
|
||||
theme (ptok/token-theme-proxy "plugin-id" file-id theme-id)
|
||||
captured (atom [])]
|
||||
(with-redefs [u/locate-token-theme
|
||||
(fn [_file _theme]
|
||||
(ctob/make-token-theme :id theme-id
|
||||
:name "Theme"
|
||||
:sets #{"Primitives"}))
|
||||
dwtl/update-token-theme
|
||||
(fn [id theme]
|
||||
(swap! captured conj {:id id :theme theme})
|
||||
:update-token-theme)
|
||||
st/emit! identity]
|
||||
(.addSet theme set)
|
||||
(.removeSet theme set)
|
||||
(t/is (= [theme-id theme-id] (mapv :id @captured)))
|
||||
(t/is (contains? (-> @captured first :theme :sets) "Primitives"))
|
||||
(t/is (not (contains? (-> @captured second :theme :sets) "Primitives"))))))
|
||||
|
||||
(t/deftest font-family-token-value-accepts-a-string
|
||||
(let [file-id (cthi/new-id! :file)
|
||||
set-id (cthi/new-id! :set)
|
||||
token-id (cthi/new-id! :token)
|
||||
captured (atom nil)]
|
||||
(with-redefs [u/locate-token (constantly {:id token-id
|
||||
:name "font.primary"
|
||||
:type :font-family
|
||||
:value ["Inter"]})
|
||||
dwtl/update-token (mock/stub (fn [set-id token-id attrs]
|
||||
(reset! captured {:set-id set-id
|
||||
:token-id token-id
|
||||
:attrs attrs})
|
||||
:update-token))
|
||||
st/emit! mock/noop]
|
||||
(let [token (ptok/token-proxy "plugin-id" file-id set-id token-id)]
|
||||
(set! (.-value token) "Inter, Arial")
|
||||
(t/is (= set-id (:set-id @captured)))
|
||||
(t/is (= token-id (:token-id @captured)))
|
||||
(t/is (= ["Inter" "Arial"] (get-in @captured [:attrs :value])))))))
|
||||
|
||||
(t/deftest typography-token-resolved-value-is-plugin-array-shape
|
||||
(let [token (ctob/make-token
|
||||
{:name "type.body"
|
||||
:type :typography
|
||||
:value {:font-family ["Inter" "Arial"]
|
||||
:font-size "16px"
|
||||
:font-weight "600"
|
||||
:line-height "20px"
|
||||
:letter-spacing "1"
|
||||
:text-case "uppercase"
|
||||
:text-decoration "underline"}})
|
||||
result (get-resolved-value token {(:name token) token})
|
||||
entry (aget result 0)]
|
||||
(t/is (array? result))
|
||||
(t/is (= ["Inter" "Arial"] (vec (aget entry "fontFamilies"))))
|
||||
(t/is (= 16 (aget entry "fontSizes")))
|
||||
(t/is (= "600" (aget entry "fontWeights")))
|
||||
(t/is (= 20 (aget entry "lineHeight")))
|
||||
(t/is (= "uppercase" (aget entry "textCase")))
|
||||
(t/is (= "underline" (aget entry "textDecoration")))))
|
||||
|
||||
(t/deftest shadow-token-resolved-value-is-plugin-array-shape
|
||||
(let [token (ctob/make-token
|
||||
{:name "shadow.card"
|
||||
:type :shadow
|
||||
:value [{:offset-x "1px"
|
||||
:offset-y "2px"
|
||||
:blur "3px"
|
||||
:spread "4px"
|
||||
:color "#000000"
|
||||
:inset false}]})
|
||||
result (get-resolved-value token {(:name token) token})
|
||||
entry (aget result 0)]
|
||||
(t/is (array? result))
|
||||
(t/is (= 1 (aget entry "offsetX")))
|
||||
(t/is (= 2 (aget entry "offsetY")))
|
||||
(t/is (= 3 (aget entry "blur")))
|
||||
(t/is (= 4 (aget entry "spread")))))
|
||||
|
||||
(t/deftest font-family-token-resolved-value-is-string-array
|
||||
(let [token (ctob/make-token
|
||||
{:name "font.primary"
|
||||
:type :font-family
|
||||
:value ["Inter" "Arial"]})
|
||||
result (get-resolved-value token {(:name token) token})]
|
||||
(t/is (array? result))
|
||||
(t/is (= ["Inter" "Arial"] (vec result)))))
|
||||
|
||||
@ -16,6 +16,8 @@
|
||||
in :fetching) and are permanently stuck with fallback-font layout metrics."
|
||||
(:require
|
||||
[app.render-wasm.api :as wasm.api]
|
||||
[app.render-wasm.mem :as mem]
|
||||
[app.render-wasm.wasm :as wasm]
|
||||
[beicon.v2.core :as rx]
|
||||
[cljs.test :as t :include-macros true]))
|
||||
|
||||
@ -108,3 +110,21 @@
|
||||
;; process-pending fires update-text-layouts, it covers shape-b too.
|
||||
(t/is (= 2 (count (:shapes @captured)))
|
||||
"Both shapes are in process-pending so font-load covers all of them")))
|
||||
|
||||
(t/deftest empty-grid-tracks-do-not-allocate-zero-bytes
|
||||
(let [calls (atom [])
|
||||
;; `h/call` is a macro that resolves the wasm function off the module
|
||||
;; via `unchecked-get`, so it cannot be redefined. Mock the module
|
||||
;; itself with recording stubs and let the real macro expansion run.
|
||||
module #js {"_set_grid_rows" (fn [& _] (swap! calls conj [:call "_set_grid_rows"]) nil)
|
||||
"_set_grid_columns" (fn [& _] (swap! calls conj [:call "_set_grid_columns"]) nil)}]
|
||||
(with-redefs [mem/alloc (fn [size]
|
||||
(swap! calls conj [:alloc size])
|
||||
0)
|
||||
wasm/internal-module module]
|
||||
(wasm.api/set-grid-layout-rows [])
|
||||
(wasm.api/set-grid-layout-columns []))
|
||||
(t/is (not-any? #(= :alloc (first %)) @calls))
|
||||
(t/is (= [[:call "_set_grid_rows"]
|
||||
[:call "_set_grid_columns"]]
|
||||
@calls))))
|
||||
|
||||
@ -27,12 +27,19 @@
|
||||
[frontend-tests.logic.groups-test]
|
||||
[frontend-tests.logic.pasting-in-containers-test]
|
||||
[frontend-tests.main-errors-test]
|
||||
[frontend-tests.plugins.comments-test]
|
||||
[frontend-tests.plugins.context-shapes-test]
|
||||
[frontend-tests.plugins.file-test]
|
||||
[frontend-tests.plugins.format-test]
|
||||
[frontend-tests.plugins.grid-test]
|
||||
[frontend-tests.plugins.interactions-test]
|
||||
[frontend-tests.plugins.library-test]
|
||||
[frontend-tests.plugins.local-storage-test]
|
||||
[frontend-tests.plugins.page-active-validation-test]
|
||||
[frontend-tests.plugins.page-test]
|
||||
[frontend-tests.plugins.parser-test]
|
||||
[frontend-tests.plugins.shape-bugfixes-test]
|
||||
[frontend-tests.plugins.text-test]
|
||||
[frontend-tests.plugins.tokens-test]
|
||||
[frontend-tests.plugins.utils-test]
|
||||
[frontend-tests.render-wasm.process-objects-test]
|
||||
@ -65,7 +72,8 @@
|
||||
(.exit js/process 1)))
|
||||
|
||||
(def test-namespaces
|
||||
['frontend-tests.basic-shapes-test
|
||||
['frontend-tests.plugins.text-test
|
||||
'frontend-tests.basic-shapes-test
|
||||
'frontend-tests.code-gen-style-test
|
||||
'frontend-tests.copy-as-svg-test
|
||||
'frontend-tests.data.nitrate-test
|
||||
@ -89,11 +97,20 @@
|
||||
'frontend-tests.logic.groups-test
|
||||
'frontend-tests.logic.pasting-in-containers-test
|
||||
'frontend-tests.plugins.context-shapes-test
|
||||
'frontend-tests.plugins.comments-test
|
||||
'frontend-tests.plugins.file-test
|
||||
'frontend-tests.plugins.format-test
|
||||
'frontend-tests.plugins.grid-test
|
||||
'frontend-tests.plugins.interactions-test
|
||||
'frontend-tests.plugins.library-test
|
||||
'frontend-tests.plugins.local-storage-test
|
||||
'frontend-tests.plugins.page-active-validation-test
|
||||
'frontend-tests.plugins.interactions-test
|
||||
'frontend-tests.plugins.format-test
|
||||
'frontend-tests.plugins.page-test
|
||||
'frontend-tests.plugins.parser-test
|
||||
'frontend-tests.plugins.shape-bugfixes-test
|
||||
'frontend-tests.plugins.text-test
|
||||
'frontend-tests.plugins.tokens-test
|
||||
'frontend-tests.plugins.utils-test
|
||||
'frontend-tests.svg-fills-test
|
||||
|
||||
@ -1,22 +1,36 @@
|
||||
## 1.5.0 (Unreleased)
|
||||
|
||||
### 💣 Breaking changes & Deprecations
|
||||
|
||||
- **plugins-runtime**: changes outside the current page now raise a validation error when the target belongs to a page that is not currently active, instead of silently operating on the active page.
|
||||
- **plugins-runtime**: Fix inverted validation that rejected valid values (and accepted invalid ones) on text range `align`, `direction`, `textDecoration`, `letterSpacing` and on layout child `zIndex`.
|
||||
- **plugins-runtime**: Array-typed properties (e.g. `page.flows`, `shape.exports`, `shape.shadows`, layout `rows`/`columns`, ruler guides, path `commands`) now always return an array, returning an empty array instead of `null` when there are no items
|
||||
- **plugin-types**: Change return type of `combineAsVariants`
|
||||
- **plugin-types:** Deprecate the legacy `Image` shape interface — image shapes exist only for backward compatibility with old files; new images are embedded in a `Fill` via its `fillImage` (an `ImageData`).
|
||||
- We've solved several inconsistencies accross the API, if you relied on an undocumented property or method be aware that might have changed.
|
||||
|
||||
### 🚀 Features
|
||||
|
||||
- **plugins-runtime**: Added `version` field that returns the current version
|
||||
- **plugins-runtime**: Added optional parameter `throwOnError` to `penpot.ui.sendMessage` (default false, backwards-compatible)
|
||||
- **plugin-types**: Added a flags subcontexts with the flag `naturalChildrenOrdering`
|
||||
- **plugin-types**: Added flag `throwValidationErrors` to enable exceptions on validation
|
||||
- **plugin-types**: `penpot.openPage()` now returns `Promise<void>` and should be awaited before performing operations on the new page
|
||||
- **plugin-types**: Fix penpot.openPage() to navigate in same tab by default
|
||||
- **plugin-types:** Change `LibraryComponent.isVariant()` return type to type guard `this is LibraryVariantComponent`
|
||||
- **plugin-types**: Added `createVariantFromComponents`
|
||||
- **plugin-types**: Change return type of `combineAsVariants`
|
||||
- **plugin-types**: Added `textBounds` property for text shapes
|
||||
- **plugin-types**: Added flag `throwValidationErrors` to enable exceptions on validation
|
||||
- **plugin-types**: Fix missing `webp` export format in `Export.type`
|
||||
- **plugin-types**: Added `fixedWhenScrolling` property for shapes
|
||||
- **plugin-runtime:** `addToken` now resolves references against all token sets, allowing references to tokens in inactive sets
|
||||
- **plugin-types:** `TokenCatalog.addSet` now accepts an optional `active` flag to create an already-active set (sets are inactive by default)
|
||||
- **plugin-runtime:** A `fontFamilies` token's `resolvedValue` now returns the documented `string[]` (the resolved family list) instead of leaking the raw tokenscript list symbol
|
||||
|
||||
### 🩹 Fixes
|
||||
|
||||
- **plugins-runtime**: Fix inverted validation that rejected valid values (and accepted invalid ones) on text range `align`, `direction`, `textDecoration`, `letterSpacing` and on layout child `zIndex`.
|
||||
- **plugins-runtime**: Array-typed properties (e.g. `page.flows`, `shape.exports`, `shape.shadows`, layout `rows`/`columns`, ruler guides, path `commands`) now always return an array, returning an empty array instead of `null` when there are no items
|
||||
- **plugin-types**: Fix penpot.openPage() to navigate in same tab by default
|
||||
- **plugin-types**: Rename `LibraryTypography.fontFamilies` to `fontFamily` to match the runtime (it holds a single font family, not an array)
|
||||
- **plugin-runtime:** Setting a `LibraryColor`'s `gradient` or `image` now clears the other color representations (solid/gradient/image are mutually exclusive), so the result is a valid color instead of being rejected with "expected valid color"
|
||||
- **plugin-types:** Mark members that have no runtime setter as `readonly`, fixing a mismatch where they were typed as writable: font metadata (`Font.*`, `FontVariant.*`, `FontsContext.all`), the `Ellipse`/`Image`/`SvgRaw` `type` discriminants (now consistent with the other shapes), `File.name`/`pages`/`revn`, `Page.root`, `TokenTheme.activeSets`, `Variants.properties`, `ImageData.*`, the board guide value objects (`GuideColumn`/`GuideRow`/`GuideSquare` and their params — `board.guides` returns a formatted snapshot, so reconfiguring means reassigning the whole array), the `Point` and `Bounds` value objects, the `Penpot.ui`/`Penpot.utils` subcontexts, the derived `Boolean` path data (`d`/`content`/`commands` are computed from the operands; `Boolean` is not editable like a `Path`), and the `EventsMap` event entries (a type-only event→callback map, never assigned). Members that do expose a setter stay writable: `Board.children`, `Path.d`/`content`/`commands` and `FileVersion.label`.
|
||||
|
||||
## 1.4.2 (2026-01-21)
|
||||
|
||||
|
||||
128
plugins/libs/plugin-types/index.d.ts
vendored
128
plugins/libs/plugin-types/index.d.ts
vendored
@ -6,7 +6,7 @@ export interface Penpot extends Omit<
|
||||
Context,
|
||||
'addListener' | 'removeListener'
|
||||
> {
|
||||
ui: {
|
||||
readonly ui: {
|
||||
/**
|
||||
* Opens the plugin UI. It is possible to develop a plugin without interface (see Palette color example) but if you need, the way to open this UI is using `penpot.ui.open`.
|
||||
* There is a minimum and maximum size for this modal and a default size but it's possible to customize it anyway with the options parameter.
|
||||
@ -84,7 +84,7 @@ export interface Penpot extends Omit<
|
||||
/**
|
||||
* Provides access to utility functions and context-specific operations.
|
||||
*/
|
||||
utils: ContextUtils;
|
||||
readonly utils: ContextUtils;
|
||||
/**
|
||||
* Closes the plugin. When this method is called the UI will be closed.
|
||||
*
|
||||
@ -390,17 +390,17 @@ export interface Boolean extends ShapeBase {
|
||||
* The content of the boolean shape, defined as the path string.
|
||||
* @deprecated Use either `d` or `commands`.
|
||||
*/
|
||||
content: string;
|
||||
readonly content: string;
|
||||
|
||||
/**
|
||||
* The content of the boolean shape, defined as the path string.
|
||||
*/
|
||||
d: string;
|
||||
readonly d: string;
|
||||
|
||||
/**
|
||||
* The content of the boolean shape, defined as an array of path commands.
|
||||
*/
|
||||
commands: Array<PathCommand>;
|
||||
readonly commands: Array<PathCommand>;
|
||||
|
||||
/**
|
||||
* The fills applied to the shape.
|
||||
@ -455,19 +455,19 @@ export type Bounds = {
|
||||
/**
|
||||
* Top-left x position of the rectangular area defined
|
||||
*/
|
||||
x: number;
|
||||
readonly x: number;
|
||||
/**
|
||||
* Top-left y position of the rectangular area defined
|
||||
*/
|
||||
y: number;
|
||||
readonly y: number;
|
||||
/**
|
||||
* Width of the represented area
|
||||
*/
|
||||
width: number;
|
||||
readonly width: number;
|
||||
/**
|
||||
* Height of the represented area
|
||||
*/
|
||||
height: number;
|
||||
readonly height: number;
|
||||
};
|
||||
|
||||
/**
|
||||
@ -1517,7 +1517,7 @@ export interface Ellipse extends ShapeBase {
|
||||
/**
|
||||
* The type of the shape, which is always 'ellipse' for ellipse shapes.
|
||||
*/
|
||||
type: 'ellipse';
|
||||
readonly type: 'ellipse';
|
||||
|
||||
/**
|
||||
* The fills applied to the shape.
|
||||
@ -1540,37 +1540,37 @@ export interface EventsMap {
|
||||
/**
|
||||
* The `pagechange` event is triggered when the active page in the project is changed.
|
||||
*/
|
||||
pagechange: Page;
|
||||
readonly pagechange: Page;
|
||||
/**
|
||||
* The `filechange` event is triggered when a different file is opened.
|
||||
* The callback will receive the new file.
|
||||
*/
|
||||
filechange: File;
|
||||
readonly filechange: File;
|
||||
/**
|
||||
* The `selectionchange` event is triggered when the selection of elements changes.
|
||||
* This event passes a list of identifiers of the selected elements.
|
||||
*/
|
||||
selectionchange: string[];
|
||||
readonly selectionchange: string[];
|
||||
/**
|
||||
* The `themechange` event is triggered when the application theme is changed.
|
||||
*/
|
||||
themechange: Theme;
|
||||
readonly themechange: Theme;
|
||||
/**
|
||||
* The `finish` event is triggered when the current file is closed.
|
||||
* The callback will receive the id of the closed file.
|
||||
*/
|
||||
finish: string;
|
||||
readonly finish: string;
|
||||
|
||||
/**
|
||||
* This event will trigger whenever the shape in the props change. It's mandatory to send
|
||||
* with the props an object like `{ shapeId: '<id>' }`
|
||||
*/
|
||||
shapechange: Shape;
|
||||
readonly shapechange: Shape;
|
||||
|
||||
/**
|
||||
* The `contentsave` event will trigger after the file content is saved in the backend.
|
||||
*/
|
||||
contentsave: void;
|
||||
readonly contentsave: void;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -1609,17 +1609,17 @@ export interface File extends PluginData {
|
||||
/**
|
||||
* The `name` for the file
|
||||
*/
|
||||
name: string;
|
||||
readonly name: string;
|
||||
|
||||
/**
|
||||
* The `revn` will change for every document update
|
||||
*/
|
||||
revn: number;
|
||||
readonly revn: number;
|
||||
|
||||
/**
|
||||
* List all the pages for the current file
|
||||
*/
|
||||
pages: Page[];
|
||||
readonly pages: Page[];
|
||||
|
||||
/**
|
||||
* Export the current file to an archive.
|
||||
@ -1819,37 +1819,37 @@ export interface Font {
|
||||
/**
|
||||
* This property holds the human-readable name of the font.
|
||||
*/
|
||||
name: string;
|
||||
readonly name: string;
|
||||
|
||||
/**
|
||||
* The unique identifier of the font.
|
||||
*/
|
||||
fontId: string;
|
||||
readonly fontId: string;
|
||||
|
||||
/**
|
||||
* The font family of the font.
|
||||
*/
|
||||
fontFamily: string;
|
||||
readonly fontFamily: string;
|
||||
|
||||
/**
|
||||
* The default font style of the font.
|
||||
*/
|
||||
fontStyle?: 'normal' | 'italic' | null;
|
||||
readonly fontStyle?: 'normal' | 'italic' | null;
|
||||
|
||||
/**
|
||||
* The default font variant ID of the font.
|
||||
*/
|
||||
fontVariantId: string;
|
||||
readonly fontVariantId: string;
|
||||
|
||||
/**
|
||||
* The default font weight of the font.
|
||||
*/
|
||||
fontWeight: string;
|
||||
readonly fontWeight: string;
|
||||
|
||||
/**
|
||||
* An array of font variants available for the font.
|
||||
*/
|
||||
variants: FontVariant[];
|
||||
readonly variants: FontVariant[];
|
||||
|
||||
/**
|
||||
* Applies the font styles to a text shape.
|
||||
@ -1884,22 +1884,22 @@ export interface FontVariant {
|
||||
/**
|
||||
* The name of the font variant.
|
||||
*/
|
||||
name: string;
|
||||
readonly name: string;
|
||||
|
||||
/**
|
||||
* The unique identifier of the font variant.
|
||||
*/
|
||||
fontVariantId: string;
|
||||
readonly fontVariantId: string;
|
||||
|
||||
/**
|
||||
* The font weight of the font variant.
|
||||
*/
|
||||
fontWeight: string;
|
||||
readonly fontWeight: string;
|
||||
|
||||
/**
|
||||
* The font style of the font variant.
|
||||
*/
|
||||
fontStyle: 'normal' | 'italic';
|
||||
readonly fontStyle: 'normal' | 'italic';
|
||||
}
|
||||
|
||||
/**
|
||||
@ -1910,7 +1910,7 @@ export interface FontsContext {
|
||||
/**
|
||||
* An array containing all available fonts.
|
||||
*/
|
||||
all: Font[];
|
||||
readonly all: Font[];
|
||||
|
||||
/**
|
||||
* Finds a font by its unique identifier.
|
||||
@ -2208,15 +2208,15 @@ export interface GuideColumn {
|
||||
/**
|
||||
* The type of the guide, which is always 'column' for column guides.
|
||||
*/
|
||||
type: 'column';
|
||||
readonly type: 'column';
|
||||
/**
|
||||
* Specifies whether the column guide is displayed.
|
||||
*/
|
||||
display: boolean;
|
||||
readonly display: boolean;
|
||||
/**
|
||||
* The parameters defining the appearance and layout of the column guides.
|
||||
*/
|
||||
params: GuideColumnParams;
|
||||
readonly params: GuideColumnParams;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -2227,7 +2227,7 @@ export interface GuideColumnParams {
|
||||
/**
|
||||
* The color configuration for the column guides.
|
||||
*/
|
||||
color: { color: string; opacity: number };
|
||||
readonly color: { color: string; opacity: number };
|
||||
/**
|
||||
* The optional alignment type of the column guides.
|
||||
* - 'stretch': Columns stretch to fit the available space.
|
||||
@ -2235,23 +2235,23 @@ export interface GuideColumnParams {
|
||||
* - 'center': Columns align to the center.
|
||||
* - 'right': Columns align to the right.
|
||||
*/
|
||||
type?: 'stretch' | 'left' | 'center' | 'right';
|
||||
readonly type?: 'stretch' | 'left' | 'center' | 'right';
|
||||
/**
|
||||
* The optional size of each column.
|
||||
*/
|
||||
size?: number;
|
||||
readonly size?: number;
|
||||
/**
|
||||
* The optional margin between the columns and the board edges.
|
||||
*/
|
||||
margin?: number;
|
||||
readonly margin?: number;
|
||||
/**
|
||||
* The optional length of each item within the columns.
|
||||
*/
|
||||
itemLength?: number;
|
||||
readonly itemLength?: number;
|
||||
/**
|
||||
* The optional gutter width between columns.
|
||||
*/
|
||||
gutter?: number;
|
||||
readonly gutter?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -2262,16 +2262,16 @@ export interface GuideRow {
|
||||
/**
|
||||
* The type of the guide, which is always 'row' for row guides.
|
||||
*/
|
||||
type: 'row';
|
||||
readonly type: 'row';
|
||||
/**
|
||||
* Specifies whether the row guide is displayed.
|
||||
*/
|
||||
display: boolean;
|
||||
readonly display: boolean;
|
||||
/**
|
||||
* The parameters defining the appearance and layout of the row guides.
|
||||
* Note: This reuses the same parameter structure as column guides.
|
||||
*/
|
||||
params: GuideColumnParams;
|
||||
readonly params: GuideColumnParams;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -2282,15 +2282,15 @@ export interface GuideSquare {
|
||||
/**
|
||||
* The type of the guide, which is always 'square' for square guides.
|
||||
*/
|
||||
type: 'square';
|
||||
readonly type: 'square';
|
||||
/**
|
||||
* Specifies whether the square guide is displayed.
|
||||
*/
|
||||
display: boolean;
|
||||
readonly display: boolean;
|
||||
/**
|
||||
* The parameters defining the appearance and layout of the square guides.
|
||||
*/
|
||||
params: GuideSquareParams;
|
||||
readonly params: GuideSquareParams;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -2301,11 +2301,11 @@ export interface GuideSquareParams {
|
||||
/**
|
||||
* The color configuration for the square guides.
|
||||
*/
|
||||
color: { color: string; opacity: number };
|
||||
readonly color: { color: string; opacity: number };
|
||||
/**
|
||||
* The optional size of each square guide.
|
||||
*/
|
||||
size?: number;
|
||||
readonly size?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -2334,12 +2334,14 @@ export interface HistoryContext {
|
||||
/**
|
||||
* Represents an image shape in Penpot.
|
||||
* This interface extends `ShapeBase` and includes properties specific to image shapes.
|
||||
* @deprecated Image shapes exist only for backward compatibility with old files.
|
||||
* New images are embedded in a `Fill` via its `fillImage` (an `ImageData`).
|
||||
*/
|
||||
export interface Image extends ShapeBase {
|
||||
/**
|
||||
* The type of the shape, which is always 'image' for image shapes.
|
||||
*/
|
||||
type: 'image';
|
||||
readonly type: 'image';
|
||||
|
||||
/**
|
||||
* The fills applied to the shape.
|
||||
@ -2355,28 +2357,28 @@ export type ImageData = {
|
||||
/**
|
||||
* The optional name of the image.
|
||||
*/
|
||||
name?: string;
|
||||
readonly name?: string;
|
||||
/**
|
||||
* The width of the image.
|
||||
*/
|
||||
width: number;
|
||||
readonly width: number;
|
||||
/**
|
||||
* The height of the image.
|
||||
*/
|
||||
height: number;
|
||||
readonly height: number;
|
||||
/**
|
||||
* The optional media type of the image (e.g., 'image/png', 'image/jpeg').
|
||||
*/
|
||||
mtype?: string;
|
||||
readonly mtype?: string;
|
||||
/**
|
||||
* The unique identifier for the image.
|
||||
*/
|
||||
id: string;
|
||||
readonly id: string;
|
||||
/**
|
||||
* Whether to keep the aspect ratio of the image when resizing.
|
||||
* Defaults to false if omitted.
|
||||
*/
|
||||
keepAspectRatio?: boolean;
|
||||
readonly keepAspectRatio?: boolean;
|
||||
|
||||
/**
|
||||
* Returns the image data as a byte array.
|
||||
@ -2870,9 +2872,9 @@ export interface LibraryTypography extends LibraryElement {
|
||||
fontId: string;
|
||||
|
||||
/**
|
||||
* The font families of the typography element.
|
||||
* The font family of the typography element.
|
||||
*/
|
||||
fontFamilies: string;
|
||||
fontFamily: string;
|
||||
|
||||
/**
|
||||
* The unique identifier of the font variant used in the typography element.
|
||||
@ -3097,7 +3099,7 @@ export interface Page extends PluginData {
|
||||
* The root shape of the current page. Will be the parent shape of all the shapes inside the document.
|
||||
* Requires `content:read` permission.
|
||||
*/
|
||||
root: Shape;
|
||||
readonly root: Shape;
|
||||
|
||||
/**
|
||||
* Retrieves a shape by its unique identifier.
|
||||
@ -3437,7 +3439,7 @@ export interface PluginData {
|
||||
/**
|
||||
* Point represents a point in 2D space, typically with x and y coordinates.
|
||||
*/
|
||||
export type Point = { x: number; y: number };
|
||||
export type Point = { readonly x: number; readonly y: number };
|
||||
|
||||
/**
|
||||
* It takes back to the last board shown.
|
||||
@ -4126,7 +4128,7 @@ export interface SvgRaw extends ShapeBase {
|
||||
/**
|
||||
* The type of the shape, which is always 'svg-raw' for raw SVG shapes.
|
||||
*/
|
||||
type: 'svg-raw';
|
||||
readonly type: 'svg-raw';
|
||||
}
|
||||
|
||||
/**
|
||||
@ -5297,7 +5299,7 @@ export interface TokenTheme {
|
||||
/**
|
||||
* The sets that will be activated if this theme is activated.
|
||||
*/
|
||||
activeSets: TokenSet[];
|
||||
readonly activeSets: TokenSet[];
|
||||
|
||||
/**
|
||||
* Adds a set to the list of the theme.
|
||||
@ -5563,7 +5565,7 @@ export interface Variants {
|
||||
/**
|
||||
* A list with the names of the properties of the Variant
|
||||
*/
|
||||
properties: string[];
|
||||
readonly properties: string[];
|
||||
|
||||
/**
|
||||
* A list of all the values of a property across all the VariantComponents of this Variant
|
||||
|
||||
@ -174,7 +174,7 @@ pub extern "C" fn set_grid_layout_data(
|
||||
#[no_mangle]
|
||||
#[wasm_error]
|
||||
pub extern "C" fn set_grid_columns() -> Result<()> {
|
||||
let bytes = mem::bytes();
|
||||
let bytes = mem::bytes_or_empty();
|
||||
|
||||
let entries: Vec<GridTrack> = bytes
|
||||
.chunks(size_of::<RawGridTrack>())
|
||||
@ -197,7 +197,7 @@ pub extern "C" fn set_grid_columns() -> Result<()> {
|
||||
#[no_mangle]
|
||||
#[wasm_error]
|
||||
pub extern "C" fn set_grid_rows() -> Result<()> {
|
||||
let bytes = mem::bytes();
|
||||
let bytes = mem::bytes_or_empty();
|
||||
|
||||
let entries: Vec<GridTrack> = bytes
|
||||
.chunks(size_of::<RawGridTrack>())
|
||||
@ -220,7 +220,7 @@ pub extern "C" fn set_grid_rows() -> Result<()> {
|
||||
#[no_mangle]
|
||||
#[wasm_error]
|
||||
pub extern "C" fn set_grid_cells() -> Result<()> {
|
||||
let bytes = mem::bytes();
|
||||
let bytes = mem::bytes_or_empty();
|
||||
|
||||
let cells: Vec<RawGridCell> = bytes
|
||||
.chunks(size_of::<RawGridCell>())
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user