0
Fork 0
mirror of https://github.com/penpot/penpot.git synced 2025-02-13 18:48:37 -05:00

Merge remote-tracking branch 'origin/staging' into develop

This commit is contained in:
Alejandro Alonso 2024-06-11 07:34:48 +02:00
commit 3bb5db6490
24 changed files with 554 additions and 129 deletions

View file

@ -33,6 +33,12 @@
- Fix expand libraries when search results are present [Taiga #7876](https://tree.taiga.io/project/penpot/issue/7876) - Fix expand libraries when search results are present [Taiga #7876](https://tree.taiga.io/project/penpot/issue/7876)
- Fix color palette default library [Taiga #8029](https://tree.taiga.io/project/penpot/issue/8029) - Fix color palette default library [Taiga #8029](https://tree.taiga.io/project/penpot/issue/8029)
- Component Library is lost after exporting/importing in .zip format [Github #4672](https://github.com/penpot/penpot/issues/4672) - Component Library is lost after exporting/importing in .zip format [Github #4672](https://github.com/penpot/penpot/issues/4672)
- Fix problem with moving+selection not working properly [Taiga #7943](https://tree.taiga.io/project/penpot/issue/7943)
- Fix problem with flex layout fit to content not positioning correctly children [Taiga #7537](https://tree.taiga.io/project/penpot/issue/7537)
- Fix black line is displaying after show main [Taiga #7653](https://tree.taiga.io/project/penpot/issue/7653)
- Fix "Share prototypes" modal remains open [Taiga #7442](https://tree.taiga.io/project/penpot/issue/7442)
- Fix "Components visibility and opacity" [#4694](https://github.com/penpot/penpot/issues/4694)
- Fix "Attribute overrides in copies are not exported in zip file" [Taiga #8072](https://tree.taiga.io/project/penpot/issue/8072)
## 2.0.3 ## 2.0.3

View file

@ -14,30 +14,38 @@
[clojure.core :as c] [clojure.core :as c]
[clojure.java.io :as io] [clojure.java.io :as io]
[cuerdas.core :as str] [cuerdas.core :as str]
[datoteka.fs :as fs]
[integrant.core :as ig])) [integrant.core :as ig]))
(defn- read-whitelist
[path]
(when (and path (fs/exists? path))
(try
(with-open [reader (io/reader path)]
(reduce (fn [result line]
(if (str/starts-with? line "#")
result
(conj result (-> line str/trim str/lower))))
#{}
(line-seq reader)))
(catch Throwable cause
(l/wrn :hint "unexpected exception on reading email whitelist"
:cause cause)))))
(defmethod ig/init-key ::email/whitelist (defmethod ig/init-key ::email/whitelist
[_ _] [_ _]
(when (c/contains? cf/flags :email-whitelist) (let [whitelist (or (cf/get :registration-domain-whitelist) #{})
(try whitelist (if (c/contains? cf/flags :email-whitelist)
(let [path (cf/get :email-domain-whitelist) (into whitelist (read-whitelist (cf/get :email-domain-whitelist)))
result (with-open [reader (io/reader path)] whitelist)
(reduce (fn [result line] whitelist (not-empty whitelist)]
(if (str/starts-with? line "#")
result
(conj result (-> line str/trim str/lower))))
#{}
(line-seq reader)))
;; backward comapatibility with previous way to set a
;; whitelist for email domains
result (into result (cf/get :registration-domain-whitelist))]
(l/inf :hint "initializing email whitelist" :domains (count result)) (when whitelist
(not-empty result)) (l/inf :hint "initializing email whitelist" :domains (count whitelist)))
(catch Throwable cause
(l/wrn :hint "unexpected exception on initializing email whitelist" whitelist))
:cause cause)))))
(defn contains? (defn contains?
"Check if email is in the whitelist." "Check if email is in the whitelist."

View file

@ -12,7 +12,6 @@
[app.common.features :as cfeat] [app.common.features :as cfeat]
[app.common.logging :as l] [app.common.logging :as l]
[app.common.schema :as sm] [app.common.schema :as sm]
[app.common.spec :as us]
[app.common.uuid :as uuid] [app.common.uuid :as uuid]
[app.config :as cf] [app.config :as cf]
[app.db :as db] [app.db :as db]
@ -32,16 +31,10 @@
[app.util.services :as sv] [app.util.services :as sv]
[app.util.time :as dt] [app.util.time :as dt]
[app.worker :as wrk] [app.worker :as wrk]
[clojure.spec.alpha :as s]
[cuerdas.core :as str])) [cuerdas.core :as str]))
;; --- Helpers & Specs ;; --- Helpers & Specs
(s/def ::id ::us/uuid)
(s/def ::name ::us/string)
(s/def ::file-id ::us/uuid)
(s/def ::team-id ::us/uuid)
(def ^:private sql:team-permissions (def ^:private sql:team-permissions
"select tpr.is_owner, "select tpr.is_owner,
tpr.is_admin, tpr.is_admin,
@ -351,7 +344,7 @@
(def ^:private schema:create-team (def ^:private schema:create-team
[:map {:title "create-team"} [:map {:title "create-team"}
[:name :string] [:name [:string {:max 250}]]
[:features {:optional true} ::cfeat/features] [:features {:optional true} ::cfeat/features]
[:id {:optional true} ::sm/uuid]]) [:id {:optional true} ::sm/uuid]])
@ -438,12 +431,14 @@
;; --- Mutation: Update Team ;; --- Mutation: Update Team
(s/def ::update-team (def ^:private schema:update-team
(s/keys :req [::rpc/profile-id] [:map {:title "update-team"}
:req-un [::name ::id])) [:name [:string {:max 250}]]
[:id ::sm/uuid]])
(sv/defmethod ::update-team (sv/defmethod ::update-team
{::doc/added "1.17"} {::doc/added "1.17"
::sm/params schema:update-team}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id id name] :as params}] [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id id name] :as params}]
(db/with-atomic [conn pool] (db/with-atomic [conn pool]
(check-edition-permissions! conn profile-id id) (check-edition-permissions! conn profile-id id)
@ -503,14 +498,14 @@
nil)) nil))
(s/def ::reassign-to ::us/uuid) (def ^:private schema:leave-team
(s/def ::leave-team [:map {:title "leave-team"}
(s/keys :req [::rpc/profile-id] [:id ::sm/uuid]
:req-un [::id] [:reassign-to {:optional true} ::sm/uuid]])
:opt-un [::reassign-to]))
(sv/defmethod ::leave-team (sv/defmethod ::leave-team
{::doc/added "1.17"} {::doc/added "1.17"
::sm/params schema:leave-team}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id] :as params}] [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id] :as params}]
(db/with-atomic [conn pool] (db/with-atomic [conn pool]
(leave-team conn (assoc params :profile-id profile-id)))) (leave-team conn (assoc params :profile-id profile-id))))
@ -539,12 +534,13 @@
:id team-id}) :id team-id})
team)) team))
(s/def ::delete-team (def ^:private schema:delete-team
(s/keys :req [::rpc/profile-id] [:map {:title "delete-team"}
:req-un [::id])) [:id ::sm/uuid]])
(sv/defmethod ::delete-team (sv/defmethod ::delete-team
{::doc/added "1.17"} {::doc/added "1.17"
::sm/params schema:delete-team}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id id] :as params}] [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id id] :as params}]
(db/with-atomic [conn pool] (db/with-atomic [conn pool]
(let [perms (get-permissions conn profile-id id)] (let [perms (get-permissions conn profile-id id)]
@ -557,10 +553,6 @@
;; --- Mutation: Team Update Role ;; --- Mutation: Team Update Role
(s/def ::team-id ::us/uuid)
(s/def ::member-id ::us/uuid)
(s/def ::role #{:owner :admin :editor})
;; Temporarily disabled viewer role ;; Temporarily disabled viewer role
;; https://tree.taiga.io/project/penpot/issue/1083 ;; https://tree.taiga.io/project/penpot/issue/1083
(def valid-roles (def valid-roles
@ -624,25 +616,29 @@
:profile-id member-id}) :profile-id member-id})
nil))) nil)))
(s/def ::update-team-member-role (def ^:private schema:update-team-member-role
(s/keys :req [::rpc/profile-id] [:map {:title "update-team-member-role"}
:req-un [::team-id ::member-id ::role])) [:team-id ::sm/uuid]
[:member-id ::sm/uuid]
[:role schema:role]])
(sv/defmethod ::update-team-member-role (sv/defmethod ::update-team-member-role
{::doc/added "1.17"} {::doc/added "1.17"
::sm/params schema:update-team-member-role}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id] :as params}] [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id] :as params}]
(db/with-atomic [conn pool] (db/with-atomic [conn pool]
(update-team-member-role conn (assoc params :profile-id profile-id)))) (update-team-member-role conn (assoc params :profile-id profile-id))))
;; --- Mutation: Delete Team Member ;; --- Mutation: Delete Team Member
(s/def ::delete-team-member (def ^:private schema:delete-team-member
(s/keys :req [::rpc/profile-id] [:map {:title "delete-team-member"}
:req-un [::team-id ::member-id])) [:team-id ::sm/uuid]
[:member-id ::sm/uuid]])
(sv/defmethod ::delete-team-member (sv/defmethod ::delete-team-member
{::doc/added "1.17"} {::doc/added "1.17"
::sm/params schema:delete-team-member}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id team-id member-id] :as params}] [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id team-id member-id] :as params}]
(db/with-atomic [conn pool] (db/with-atomic [conn pool]
(let [perms (get-permissions conn profile-id team-id)] (let [perms (get-permissions conn profile-id team-id)]
@ -665,13 +661,14 @@
(declare upload-photo) (declare upload-photo)
(declare ^:private update-team-photo) (declare ^:private update-team-photo)
(s/def ::file ::media/upload) (def ^:private schema:update-team-photo
(s/def ::update-team-photo [:map {:title "update-team-photo"}
(s/keys :req [::rpc/profile-id] [:team-id ::sm/uuid]
:req-un [::team-id ::file])) [:file ::media/upload]])
(sv/defmethod ::update-team-photo (sv/defmethod ::update-team-photo
{::doc/added "1.17"} {::doc/added "1.17"
::sm/params schema:update-team-photo}
[cfg {:keys [::rpc/profile-id file] :as params}] [cfg {:keys [::rpc/profile-id file] :as params}]
;; Validate incoming mime type ;; Validate incoming mime type
(media/validate-media-type! file #{"image/jpeg" "image/png" "image/webp"}) (media/validate-media-type! file #{"image/jpeg" "image/png" "image/webp"})
@ -809,7 +806,7 @@
(def ^:private schema:create-team-invitations (def ^:private schema:create-team-invitations
[:map {:title "create-team-invitations"} [:map {:title "create-team-invitations"}
[:team-id ::sm/uuid] [:team-id ::sm/uuid]
[:role [::sm/one-of #{:owner :admin :editor}]] [:role schema:role]
[:emails ::sm/set-of-emails]]) [:emails ::sm/set-of-emails]])
(sv/defmethod ::create-team-invitations (sv/defmethod ::create-team-invitations
@ -866,12 +863,6 @@
;; --- Mutation: Create Team & Invite Members ;; --- Mutation: Create Team & Invite Members
(s/def ::emails ::us/set-of-valid-emails)
(s/def ::create-team-with-invitations
(s/merge ::create-team
(s/keys :req-un [::emails ::role])))
(def ^:private schema:create-team-with-invitations (def ^:private schema:create-team-with-invitations
[:map {:title "create-team-with-invitations"} [:map {:title "create-team-with-invitations"}
[:name :string] [:name :string]
@ -930,12 +921,14 @@
;; --- Query: get-team-invitation-token ;; --- Query: get-team-invitation-token
(s/def ::get-team-invitation-token (def ^:private schema:get-team-invitation-token
(s/keys :req [::rpc/profile-id] [:map {:title "get-team-invitation-token"}
:req-un [::team-id ::email])) [:team-id ::sm/uuid]
[:email ::sm/email]])
(sv/defmethod ::get-team-invitation-token (sv/defmethod ::get-team-invitation-token
{::doc/added "1.17"} {::doc/added "1.17"
::sm/params schema:get-team-invitation-token}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id team-id email] :as params}] [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id team-id email] :as params}]
(check-read-permissions! pool profile-id team-id) (check-read-permissions! pool profile-id team-id)
(let [email (profile/clean-email email) (let [email (profile/clean-email email)
@ -956,12 +949,15 @@
;; --- Mutation: Update invitation role ;; --- Mutation: Update invitation role
(s/def ::update-team-invitation-role (def ^:private schema:update-team-invitation-role
(s/keys :req [::rpc/profile-id] [:map {:title "update-team-invitation-role"}
:req-un [::team-id ::email ::role])) [:team-id ::sm/uuid]
[:email ::sm/email]
[:role schema:role]])
(sv/defmethod ::update-team-invitation-role (sv/defmethod ::update-team-invitation-role
{::doc/added "1.17"} {::doc/added "1.17"
::sm/params schema:update-team-invitation-role}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id team-id email role] :as params}] [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id team-id email role] :as params}]
(db/with-atomic [conn pool] (db/with-atomic [conn pool]
(let [perms (get-permissions conn profile-id team-id)] (let [perms (get-permissions conn profile-id team-id)]
@ -977,12 +973,14 @@
;; --- Mutation: Delete invitation ;; --- Mutation: Delete invitation
(s/def ::delete-team-invitation (def ^:private schema:delete-team-invition
(s/keys :req [::rpc/profile-id] [:map {:title "delete-team-invitation"}
:req-un [::team-id ::email])) [:team-id ::sm/uuid]
[:email ::sm/email]])
(sv/defmethod ::delete-team-invitation (sv/defmethod ::delete-team-invitation
{::doc/added "1.17"} {::doc/added "1.17"
::sm/params schema:delete-team-invition}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id team-id email] :as params}] [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id team-id email] :as params}]
(db/with-atomic [conn pool] (db/with-atomic [conn pool]
(let [perms (get-permissions conn profile-id team-id)] (let [perms (get-permissions conn profile-id team-id)]

View file

@ -8,18 +8,19 @@
"Tokens generation API." "Tokens generation API."
(:require (:require
[app.common.data :as d] [app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.exceptions :as ex] [app.common.exceptions :as ex]
[app.common.spec :as us]
[app.common.transit :as t] [app.common.transit :as t]
[app.util.time :as dt] [app.util.time :as dt]
[buddy.sign.jwe :as jwe] [buddy.sign.jwe :as jwe]))
[clojure.spec.alpha :as s]))
(s/def ::tokens-key bytes?)
(defn generate (defn generate
[{:keys [tokens-key]} claims] [{:keys [tokens-key]} claims]
(us/assert! ::tokens-key tokens-key)
(dm/assert!
"expexted token-key to be bytes instance"
(bytes? tokens-key))
(let [payload (-> claims (let [payload (-> claims
(assoc :iat (dt/now)) (assoc :iat (dt/now))
(d/without-nils) (d/without-nils)
@ -39,15 +40,13 @@
(ex/raise :type :validation (ex/raise :type :validation
:code :invalid-token :code :invalid-token
:reason :token-expired :reason :token-expired
:params params :params params))
:claims claims))
(when (and (contains? params :iss) (when (and (contains? params :iss)
(not= (:iss claims) (not= (:iss claims)
(:iss params))) (:iss params)))
(ex/raise :type :validation (ex/raise :type :validation
:code :invalid-token :code :invalid-token
:reason :invalid-issuer :reason :invalid-issuer
:claims claims
:params params)) :params params))
claims)) claims))

View file

@ -224,7 +224,6 @@
[coll] [coll]
(into [] (remove nil?) coll)) (into [] (remove nil?) coll))
(defn without-nils (defn without-nils
"Given a map, return a map removing key-value "Given a map, return a map removing key-value
pairs when value is `nil`." pairs when value is `nil`."

View file

@ -269,6 +269,13 @@
(keep (mk-check-auto-layout objects)) (keep (mk-check-auto-layout objects))
shapes))) shapes)))
(defn full-tree?
"Checks if we need to calculate the full tree or we can calculate just a partial tree. Partial
trees are more efficient but cannot be done when the layout is centered."
[objects layout-id]
(let [layout-justify-content (get-in objects [layout-id :layout-justify-content])]
(contains? #{:center :end :space-around :space-evenly :stretch} layout-justify-content)))
(defn sizing-auto-modifiers (defn sizing-auto-modifiers
"Recalculates the layouts to adjust the sizing: auto new sizes" "Recalculates the layouts to adjust the sizing: auto new sizes"
[modif-tree sizing-auto-layouts objects bounds ignore-constraints] [modif-tree sizing-auto-layouts objects bounds ignore-constraints]
@ -286,7 +293,7 @@
(d/seek sizing-auto-layouts)) (d/seek sizing-auto-layouts))
shapes shapes
(if from-layout (if (and from-layout (not (full-tree? objects from-layout)))
(cgst/resolve-subtree from-layout layout-id objects) (cgst/resolve-subtree from-layout layout-id objects)
(cgst/resolve-tree #{layout-id} objects)) (cgst/resolve-tree #{layout-id} objects))

View file

@ -189,14 +189,20 @@
(when swap-slot (when swap-slot
(keyword (str "swap-slot-" swap-slot)))) (keyword (str "swap-slot-" swap-slot))))
(defn swap-slot?
[group]
(str/starts-with? (name group) "swap-slot-"))
(defn group->swap-slot
[group]
(uuid/uuid (subs (name group) 10)))
(defn get-swap-slot (defn get-swap-slot
"If the shape has a :touched group in the form :swap-slot-<uuid>, get the id." "If the shape has a :touched group in the form :swap-slot-<uuid>, get the id."
[shape] [shape]
(let [group (->> (:touched shape) (let [group (d/seek swap-slot? (:touched shape))]
(map name)
(d/seek #(str/starts-with? % "swap-slot-")))]
(when group (when group
(uuid/uuid (subs group 10))))) (group->swap-slot group))))
(defn match-swap-slot? (defn match-swap-slot?
[shape-main shape-inst] [shape-main shape-inst]
@ -264,3 +270,16 @@
;; Non instance, non copy. We allow ;; Non instance, non copy. We allow
(or (not (instance-head? shape)) (or (not (instance-head? shape))
(not (in-component-copy? parent)))))) (not (in-component-copy? parent))))))
(defn all-touched-groups
[]
(into #{} (vals sync-attrs)))
(defn valid-touched-group?
[group]
(try
(or ((all-touched-groups) group)
(and (swap-slot? group)
(some? (group->swap-slot group))))
(catch #?(:clj Throwable :cljs :default) _
false)))

View file

@ -0,0 +1,43 @@
;; 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 common-tests.types.types-component-test
(:require
[app.common.test-helpers.ids-map :as thi]
[app.common.test-helpers.shapes :as ths]
[app.common.types.component :as ctk]
[clojure.test :as t]))
(t/use-fixtures :each thi/test-fixture)
(t/deftest test-valid-touched-group
(t/is (ctk/valid-touched-group? :name-group))
(t/is (ctk/valid-touched-group? :geometry-group))
(t/is (ctk/valid-touched-group? :swap-slot-9cc181fa-5eef-8084-8004-7bb2ab45fd1f))
(t/is (not (ctk/valid-touched-group? :this-is-not-a-group)))
(t/is (not (ctk/valid-touched-group? :swap-slot-)))
(t/is (not (ctk/valid-touched-group? :swap-slot-xxxxxx)))
(t/is (not (ctk/valid-touched-group? :swap-slot-9cc181fa-5eef-8084-8004)))
(t/is (not (ctk/valid-touched-group? nil))))
(t/deftest test-get-swap-slot
(let [s1 (ths/sample-shape :s1)
s2 (ths/sample-shape :s2 :touched #{:visibility-group})
s3 (ths/sample-shape :s3 :touched #{:swap-slot-9cc181fa-5eef-8084-8004-7bb2ab45fd1f})
s4 (ths/sample-shape :s4 :touched #{:fill-group
:swap-slot-9cc181fa-5eef-8084-8004-7bb2ab45fd1f})
s5 (ths/sample-shape :s5 :touched #{:swap-slot-9cc181fa-5eef-8084-8004-7bb2ab45fd1f
:content-group
:geometry-group})
s6 (ths/sample-shape :s6 :touched #{:swap-slot-9cc181fa})]
(t/is (nil? (ctk/get-swap-slot s1)))
(t/is (nil? (ctk/get-swap-slot s2)))
(t/is (= (ctk/get-swap-slot s3) #uuid "9cc181fa-5eef-8084-8004-7bb2ab45fd1f"))
(t/is (= (ctk/get-swap-slot s4) #uuid "9cc181fa-5eef-8084-8004-7bb2ab45fd1f"))
(t/is (= (ctk/get-swap-slot s5) #uuid "9cc181fa-5eef-8084-8004-7bb2ab45fd1f"))
#?(:clj
(t/is (thrown-with-msg? IllegalArgumentException #"Invalid UUID string"
(ctk/get-swap-slot s6))))))

View file

@ -0,0 +1,186 @@
{
"~:id": "~udd5cc0bb-91ff-81b9-8004-77dfae2d9e7c",
"~:file-id": "~udd5cc0bb-91ff-81b9-8004-77df9cd3edb1",
"~:created-at": "~m1717759268004",
"~:content": {
"~:options": {},
"~:objects": {
"~u00000000-0000-0000-0000-000000000000": {
"~#shape": {
"~:y": 0,
"~:hide-fill-on-export": false,
"~:transform": {
"~#matrix": {
"~:a": 1.0,
"~:b": 0.0,
"~:c": 0.0,
"~:d": 1.0,
"~:e": 0.0,
"~:f": 0.0
}
},
"~:rotation": 0,
"~:name": "Root Frame",
"~:width": 0.01,
"~:type": "~:frame",
"~:points": [
{
"~#point": {
"~:x": 0,
"~:y": 0
}
},
{
"~#point": {
"~:x": 0.01,
"~:y": 0
}
},
{
"~#point": {
"~:x": 0.01,
"~:y": 0.01
}
},
{
"~#point": {
"~:x": 0,
"~:y": 0.01
}
}
],
"~:proportion-lock": false,
"~:transform-inverse": {
"~#matrix": {
"~:a": 1.0,
"~:b": 0.0,
"~:c": 0.0,
"~:d": 1.0,
"~:e": 0.0,
"~:f": 0.0
}
},
"~:id": "~u00000000-0000-0000-0000-000000000000",
"~:parent-id": "~u00000000-0000-0000-0000-000000000000",
"~:frame-id": "~u00000000-0000-0000-0000-000000000000",
"~:strokes": [],
"~:x": 0,
"~:proportion": 1.0,
"~:selrect": {
"~#rect": {
"~:x": 0,
"~:y": 0,
"~:width": 0.01,
"~:height": 0.01,
"~:x1": 0,
"~:y1": 0,
"~:x2": 0.01,
"~:y2": 0.01
}
},
"~:fills": [
{
"~:fill-color": "#FFFFFF",
"~:fill-opacity": 1
}
],
"~:flip-x": null,
"~:height": 0.01,
"~:flip-y": null,
"~:shapes": [
"~uec508673-9e3b-80bf-8004-77dfa30a2b13"
]
}
},
"~uec508673-9e3b-80bf-8004-77dfa30a2b13": {
"~#shape": {
"~:y": 0,
"~:hide-fill-on-export": false,
"~:transform": {
"~#matrix": {
"~:a": 1.0,
"~:b": 0.0,
"~:c": 0.0,
"~:d": 1.0,
"~:e": 0.0,
"~:f": 0.0
}
},
"~:rotation": 0,
"~:grow-type": "~:fixed",
"~:hide-in-viewer": false,
"~:name": "Board",
"~:width": 256.00000000000006,
"~:type": "~:frame",
"~:points": [
{
"~#point": {
"~:x": 0,
"~:y": 0
}
},
{
"~#point": {
"~:x": 256.00000000000006,
"~:y": 0
}
},
{
"~#point": {
"~:x": 256.00000000000006,
"~:y": 256
}
},
{
"~#point": {
"~:x": 0,
"~:y": 256
}
}
],
"~:proportion-lock": false,
"~:transform-inverse": {
"~#matrix": {
"~:a": 1.0,
"~:b": 0.0,
"~:c": 0.0,
"~:d": 1.0,
"~:e": 0.0,
"~:f": 0.0
}
},
"~:id": "~uec508673-9e3b-80bf-8004-77dfa30a2b13",
"~:parent-id": "~u00000000-0000-0000-0000-000000000000",
"~:frame-id": "~u00000000-0000-0000-0000-000000000000",
"~:strokes": [],
"~:x": 0,
"~:proportion": 1,
"~:selrect": {
"~#rect": {
"~:x": 0,
"~:y": 0,
"~:width": 256.00000000000006,
"~:height": 256,
"~:x1": 0,
"~:y1": 0,
"~:x2": 256.00000000000006,
"~:y2": 256
}
},
"~:fills": [
{
"~:fill-color": "#FFFFFF",
"~:fill-opacity": 1
}
],
"~:flip-x": null,
"~:height": 256,
"~:flip-y": null,
"~:shapes": []
}
}
},
"~:id": "~udd5cc0bb-91ff-81b9-8004-77df9cd3edb2",
"~:name": "Page 1"
}
}

View file

@ -0,0 +1,86 @@
{
"~:users": [
{
"~:id": "~u0515a066-e303-8169-8004-73eb4018f4e0",
"~:email": "leia@example.com",
"~:name": "Princesa Leia",
"~:fullname": "Princesa Leia",
"~:is-active": true
}
],
"~:fonts": [],
"~:project": {
"~:id": "~u0515a066-e303-8169-8004-73eb401b5d55",
"~:name": "Drafts",
"~:team-id": "~u0515a066-e303-8169-8004-73eb401977a6"
},
"~:share-links": [],
"~:libraries": [],
"~:file": {
"~:features": {
"~#set": [
"layout/grid",
"styles/v2",
"fdata/pointer-map",
"fdata/objects-map",
"components/v2",
"fdata/shape-data-type"
]
},
"~:has-media-trimmed": false,
"~:comment-thread-seqn": 0,
"~:name": "New File 3",
"~:revn": 1,
"~:modified-at": "~m1717759268010",
"~:id": "~udd5cc0bb-91ff-81b9-8004-77df9cd3edb1",
"~:is-shared": false,
"~:version": 48,
"~:project-id": "~u0515a066-e303-8169-8004-73eb401b5d55",
"~:created-at": "~m1717759250257",
"~:data": {
"~:id": "~udd5cc0bb-91ff-81b9-8004-77df9cd3edb1",
"~:options": {
"~:components-v2": true
},
"~:pages": [
"~udd5cc0bb-91ff-81b9-8004-77df9cd3edb2"
],
"~:pages-index": {
"~udd5cc0bb-91ff-81b9-8004-77df9cd3edb2": {
"~#penpot/pointer": [
"~udd5cc0bb-91ff-81b9-8004-77dfae2d9e7c",
{
"~:created-at": "~m1717759268024"
}
]
}
}
}
},
"~:team": {
"~:id": "~u0515a066-e303-8169-8004-73eb401977a6",
"~:created-at": "~m1717493865581",
"~:modified-at": "~m1717493865581",
"~:name": "Default",
"~:is-default": true,
"~:features": {
"~#set": [
"layout/grid",
"styles/v2",
"fdata/pointer-map",
"fdata/objects-map",
"components/v2",
"fdata/shape-data-type"
]
}
},
"~:permissions": {
"~:type": "~:membership",
"~:is-owner": true,
"~:is-admin": true,
"~:can-edit": true,
"~:can-read": true,
"~:is-logged": true,
"~:in-team": true
}
}

View file

@ -33,10 +33,8 @@ export class ViewerPage extends BaseWebSocketPage {
super(page); super(page);
} }
async goToViewer() { async goToViewer({ fileId = ViewerPage.anyFileId, pageId = ViewerPage.anyPageId } = {}) {
await this.page.goto( await this.page.goto(`/#/view/${fileId}?page-id=${pageId}&section=interactions&index=0`);
`/#/view/${ViewerPage.anyFileId}?page-id=${ViewerPage.anyPageId}&section=interactions&index=0`,
);
this.#ws = await this.waitForNotificationsWebSocket(); this.#ws = await this.waitForNotificationsWebSocket();
await this.#ws.mockOpen(); await this.#ws.mockOpen();

View file

@ -111,7 +111,7 @@ export class WorkspacePage extends BaseWebSocketPage {
const layer = this.layers.getByTestId("layer-item").filter({ has: this.page.getByText(name) }); const layer = this.layers.getByTestId("layer-item").filter({ has: this.page.getByText(name) });
await layer.getByRole("button").click(clickOptions); await layer.getByRole("button").click(clickOptions);
} }
async clickAssets(clickOptions = {}) { async clickAssets(clickOptions = {}) {
await this.assets.click(clickOptions); await this.assets.click(clickOptions);
} }

View file

@ -5,6 +5,18 @@ test.beforeEach(async ({ page }) => {
await ViewerPage.init(page); await ViewerPage.init(page);
}); });
const singleBoardFileId = "dd5cc0bb-91ff-81b9-8004-77df9cd3edb1";
const singleBoardPageId = "dd5cc0bb-91ff-81b9-8004-77df9cd3edb2";
const setupFileWithSingleBoard = async (viewer) => {
await viewer.mockRPC(/get\-view\-only\-bundle\?/, "viewer/get-view-only-bundle-single-board.json");
await viewer.mockRPC("get-comment-threads?file-id=*", "workspace/get-comment-threads-empty.json");
await viewer.mockRPC(
"get-file-fragment?file-id=*&fragment-id=*",
"viewer/get-file-fragment-single-board.json",
);
};
test("Clips link area of the logo", async ({ page }) => { test("Clips link area of the logo", async ({ page }) => {
const viewerPage = new ViewerPage(page); const viewerPage = new ViewerPage(page);
await viewerPage.setupLoggedInUser(); await viewerPage.setupLoggedInUser();
@ -21,3 +33,16 @@ test("Clips link area of the logo", async ({ page }) => {
await viewerPage.page.mouse.click(x, y + 100); await viewerPage.page.mouse.click(x, y + 100);
await expect(page.url()).toBe(viewerUrl); await expect(page.url()).toBe(viewerUrl);
}); });
test("Updates URL with zoom type", async ({ page }) => {
const viewer = new ViewerPage(page);
await viewer.setupLoggedInUser();
await setupFileWithSingleBoard(viewer);
await viewer.goToViewer({ fileId: singleBoardFileId, pageId: singleBoardPageId });
await viewer.page.getByTitle("Zoom").click();
await viewer.page.getByText(/Fit/).click();
await expect(viewer.page).toHaveURL(/&zoom=fit/);
});

View file

@ -69,9 +69,10 @@
(cpc/check-changes! undo-changes))) (cpc/check-changes! undo-changes)))
(let [commit-id (or commit-id (uuid/next)) (let [commit-id (or commit-id (uuid/next))
source (d/nilv source :local)
commit {:id commit-id commit {:id commit-id
:created-at (dt/now) :created-at (dt/now)
:source (d/nilv source :local) :source source
:origin (ptk/type origin) :origin (ptk/type origin)
:features features :features features
:file-id file-id :file-id file-id
@ -110,9 +111,7 @@
redo-changes (if pending redo-changes (if pending
(into redo-changes (into redo-changes
(comp (mapcat :redo-changes)
(map :redo-changes)
(mapcat identity))
pending) pending)
redo-changes)] redo-changes)]

View file

@ -182,7 +182,7 @@
(log/debug :hint "initialize persistence") (log/debug :hint "initialize persistence")
(let [stoper-s (rx/filter (ptk/type? ::initialize-persistence) stream) (let [stoper-s (rx/filter (ptk/type? ::initialize-persistence) stream)
commits-s local-commits-s
(->> stream (->> stream
(rx/filter dch/commit?) (rx/filter dch/commit?)
(rx/map deref) (rx/map deref)
@ -192,28 +192,34 @@
notifier-s notifier-s
(rx/merge (rx/merge
(->> commits-s (->> local-commits-s
(rx/debounce 3000) (rx/debounce 3000)
(rx/tap #(log/trc :hint "persistence beat"))) (rx/tap #(log/trc :hint "persistence beat")))
(->> stream (->> stream
(rx/filter #(= % ::force-persist))))] (rx/filter #(= % ::force-persist))))]
(rx/merge (rx/merge
(->> commits-s (->> local-commits-s
(rx/debounce 200) (rx/debounce 200)
(rx/map (fn [_] (rx/map (fn [_]
(update-status :pending))) (update-status :pending)))
(rx/take-until stoper-s)) (rx/take-until stoper-s))
(->> local-commits-s
(rx/buffer-time 200)
(rx/mapcat merge-commit)
(rx/map dch/update-indexes)
(rx/take-until stoper-s)
(rx/finalize (fn []
(log/debug :hint "finalize persistence: changes watcher [index]"))))
;; Here we watch for local commits, buffer them in a small ;; Here we watch for local commits, buffer them in a small
;; chunks (very near in time commits) and append them to the ;; chunks (very near in time commits) and append them to the
;; persistence queue ;; persistence queue
(->> commits-s (->> local-commits-s
(rx/buffer-until notifier-s) (rx/buffer-until notifier-s)
(rx/mapcat merge-commit) (rx/mapcat merge-commit)
(rx/mapcat (fn [commit] (rx/map append-commit)
(rx/of (append-commit commit)
(dch/update-indexes commit))))
(rx/take-until (rx/delay 100 stoper-s)) (rx/take-until (rx/delay 100 stoper-s))
(rx/finalize (fn [] (rx/finalize (fn []
(log/debug :hint "finalize persistence: changes watcher")))) (log/debug :hint "finalize persistence: changes watcher"))))

View file

@ -253,6 +253,18 @@
;; --- Zoom Management ;; --- Zoom Management
(def update-zoom-querystring
(ptk/reify ::update-zoom-querystring
ptk/WatchEvent
(watch [_ state _]
(let [zoom-type (get-in state [:viewer-local :zoom-type])
route (:route state)
screen (-> route :data :name keyword)
qparams (:query-params route)
pparams (:path-params route)]
(rx/of (rt/nav screen pparams (assoc qparams :zoom zoom-type)))))))
(def increase-zoom (def increase-zoom
(ptk/reify ::increase-zoom (ptk/reify ::increase-zoom
ptk/UpdateEvent ptk/UpdateEvent
@ -293,7 +305,10 @@
minzoom (min wdiff hdiff)] minzoom (min wdiff hdiff)]
(-> state (-> state
(assoc-in [:viewer-local :zoom] minzoom) (assoc-in [:viewer-local :zoom] minzoom)
(assoc-in [:viewer-local :zoom-type] :fit)))))) (assoc-in [:viewer-local :zoom-type] :fit))))
ptk/WatchEvent
(watch [_ _ _] (rx/of update-zoom-querystring))))
(def zoom-to-fill (def zoom-to-fill
(ptk/reify ::zoom-to-fill (ptk/reify ::zoom-to-fill
@ -309,7 +324,9 @@
maxzoom (max wdiff hdiff)] maxzoom (max wdiff hdiff)]
(-> state (-> state
(assoc-in [:viewer-local :zoom] maxzoom) (assoc-in [:viewer-local :zoom] maxzoom)
(assoc-in [:viewer-local :zoom-type] :fill)))))) (assoc-in [:viewer-local :zoom-type] :fill))))
ptk/WatchEvent
(watch [_ _ _] (rx/of update-zoom-querystring))))
(def toggle-zoom-style (def toggle-zoom-style
(ptk/reify ::toggle-zoom-style (ptk/reify ::toggle-zoom-style

View file

@ -431,7 +431,7 @@
(watch [_ state stream] (watch [_ state stream]
(let [initial (deref ms/mouse-position) (let [initial (deref ms/mouse-position)
stopper (mse/drag-stopper stream) stopper (mse/drag-stopper stream {:interrupt? false})
zoom (get-in state [:workspace-local :zoom] 1) zoom (get-in state [:workspace-local :zoom] 1)
;; We toggle the selection so we don't have to wait for the event ;; We toggle the selection so we don't have to wait for the event

View file

@ -149,7 +149,7 @@
svg-raw-wrapper (mf/use-memo (mf/deps objects) #(svg-raw-wrapper-factory objects)) svg-raw-wrapper (mf/use-memo (mf/deps objects) #(svg-raw-wrapper-factory objects))
bool-wrapper (mf/use-memo (mf/deps objects) #(bool-wrapper-factory objects)) bool-wrapper (mf/use-memo (mf/deps objects) #(bool-wrapper-factory objects))
frame-wrapper (mf/use-memo (mf/deps objects) #(frame-wrapper-factory objects))] frame-wrapper (mf/use-memo (mf/deps objects) #(frame-wrapper-factory objects))]
(when (and shape (not (:hidden shape))) (when shape
(let [opts #js {:shape shape} (let [opts #js {:shape shape}
svg-raw? (= :svg-raw (:type shape))] svg-raw? (= :svg-raw (:type shape))]
(if-not svg-raw? (if-not svg-raw?

View file

@ -50,6 +50,9 @@
(defn bool->str [val] (defn bool->str [val]
(when (some? val) (str val))) (when (some? val) (str val)))
(defn touched->str [val]
(str/join " " (map str val)))
(defn add-factory [shape] (defn add-factory [shape]
(fn add! (fn add!
([props attr] ([props attr]
@ -136,7 +139,6 @@
(cond-> bool? (cond-> bool?
(add! :bool-type))))) (add! :bool-type)))))
(defn add-library-refs [props shape] (defn add-library-refs [props shape]
(let [add! (add-factory shape)] (let [add! (add-factory shape)]
(-> props (-> props
@ -150,7 +152,8 @@
(add! :component-id) (add! :component-id)
(add! :component-root) (add! :component-root)
(add! :main-instance) (add! :main-instance)
(add! :shape-ref)))) (add! :shape-ref)
(add! :touched touched->str))))
(defn prefix-keys [m] (defn prefix-keys [m]
(letfn [(prefix-entry [[k v]] (letfn [(prefix-entry [[k v]]

View file

@ -72,6 +72,8 @@
(obj/set! "pointerEvents" pointer-events) (obj/set! "pointerEvents" pointer-events)
(cond-> (not (cfh/frame-shape? shape)) (cond-> (not (cfh/frame-shape? shape))
(obj/set! "opacity" (:opacity shape))) (obj/set! "opacity" (:opacity shape)))
(cond-> (:hidden shape)
(obj/set! "display" "none"))
(cond-> (and blend-mode (not= blend-mode :normal)) (cond-> (and blend-mode (not= blend-mode :normal))
(obj/set! "mixBlendMode" (d/name blend-mode)))) (obj/set! "mixBlendMode" (d/name blend-mode))))

View file

@ -138,7 +138,7 @@
(mf/deps page) (mf/deps page)
(fn [] (fn []
(modal/show! :share-link {:page page :file file}) (modal/show! :share-link {:page page :file file})
(modal/allow-click-outside!))) (modal/disallow-click-outside!)))
handle-increase handle-increase
(mf/use-fn (mf/use-fn

View file

@ -402,9 +402,11 @@
(st/emit! (dwl/nav-to-component-file library-id comp)))) (st/emit! (dwl/nav-to-component-file library-id comp))))
do-show-component do-show-component
#(if local-component? (fn []
(do-show-local-component) (st/emit! dw/hide-context-menu)
(do-show-remote-component)) (if local-component?
(do-show-local-component)
(do-show-remote-component)))
do-restore-component do-restore-component
#(let [;; Extract a map of component-id -> component-file in order to avoid duplicates #(let [;; Extract a map of component-id -> component-file in order to avoid duplicates

View file

@ -72,12 +72,20 @@
(defn drag-stopper (defn drag-stopper
"Creates a stream to stop drag events. Takes into account the mouse and also "Creates a stream to stop drag events. Takes into account the mouse and also
if the window loses focus or the esc key is pressed." if the window loses focus or the esc key is pressed."
[stream] ([stream]
(rx/merge (drag-stopper stream nil))
(->> stream ([stream {:keys [blur? up-mouse? interrupt?] :or {blur? true up-mouse? true interrupt? true}}]
(rx/filter blur-event?)) (rx/merge
(->> stream (if blur?
(rx/filter mouse-event?) (->> stream
(rx/filter mouse-up-event?)) (rx/filter blur-event?))
(->> stream (rx/empty))
(rx/filter #(= % :interrupt))))) (if up-mouse?
(->> stream
(rx/filter mouse-event?)
(rx/filter mouse-up-event?))
(rx/empty))
(if interrupt?
(->> stream
(rx/filter #(= % :interrupt)))
(rx/empty)))))

View file

@ -12,6 +12,7 @@
[app.common.geom.matrix :as gmt] [app.common.geom.matrix :as gmt]
[app.common.geom.point :as gpt] [app.common.geom.point :as gpt]
[app.common.svg.path :as svg.path] [app.common.svg.path :as svg.path]
[app.common.types.component :as ctk]
[app.common.types.shape.interactions :as ctsi] [app.common.types.shape.interactions :as ctsi]
[app.common.uuid :as uuid] [app.common.uuid :as uuid]
[app.util.json :as json] [app.util.json :as json]
@ -129,6 +130,15 @@
(into {})) (into {}))
style-str)) style-str))
(defn parse-touched
"Transform a string of :touched-groups into a set"
[touched-str]
(let [touched (->> (str/split touched-str " ")
(map #(keyword (subs % 1)))
(filter ctk/valid-touched-group?)
(into #{}))]
touched))
(defn add-attrs (defn add-attrs
[m attrs] [m attrs]
(reduce-kv (reduce-kv
@ -424,7 +434,8 @@
component-file (get-meta node :component-file uuid/uuid) component-file (get-meta node :component-file uuid/uuid)
shape-ref (get-meta node :shape-ref uuid/uuid) shape-ref (get-meta node :shape-ref uuid/uuid)
component-root? (get-meta node :component-root str->bool) component-root? (get-meta node :component-root str->bool)
main-instance? (get-meta node :main-instance str->bool)] main-instance? (get-meta node :main-instance str->bool)
touched (get-meta node :touched parse-touched)]
(cond-> props (cond-> props
(some? stroke-color-ref-id) (some? stroke-color-ref-id)
@ -442,7 +453,10 @@
(assoc :main-instance main-instance?) (assoc :main-instance main-instance?)
(some? shape-ref) (some? shape-ref)
(assoc :shape-ref shape-ref)))) (assoc :shape-ref shape-ref)
(seq touched)
(assoc :touched touched))))
(defn add-fill (defn add-fill
[props node svg-data] [props node svg-data]