diff --git a/CHANGES.md b/CHANGES.md index a875e37b3..8b351fcb6 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -33,6 +33,12 @@ - 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) - 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 diff --git a/backend/src/app/email/whitelist.clj b/backend/src/app/email/whitelist.clj index d6fbd0c85..85c137bfb 100644 --- a/backend/src/app/email/whitelist.clj +++ b/backend/src/app/email/whitelist.clj @@ -14,30 +14,38 @@ [clojure.core :as c] [clojure.java.io :as io] [cuerdas.core :as str] + [datoteka.fs :as fs] [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 [_ _] - (when (c/contains? cf/flags :email-whitelist) - (try - (let [path (cf/get :email-domain-whitelist) - result (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))) + (let [whitelist (or (cf/get :registration-domain-whitelist) #{}) + whitelist (if (c/contains? cf/flags :email-whitelist) + (into whitelist (read-whitelist (cf/get :email-domain-whitelist))) + whitelist) + whitelist (not-empty whitelist)] - ;; 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)) - (not-empty result)) - (catch Throwable cause - (l/wrn :hint "unexpected exception on initializing email whitelist" - :cause cause))))) + (when whitelist + (l/inf :hint "initializing email whitelist" :domains (count whitelist))) + + whitelist)) (defn contains? "Check if email is in the whitelist." diff --git a/backend/src/app/rpc/commands/teams.clj b/backend/src/app/rpc/commands/teams.clj index 4cc75de5a..8eb41386f 100644 --- a/backend/src/app/rpc/commands/teams.clj +++ b/backend/src/app/rpc/commands/teams.clj @@ -12,7 +12,6 @@ [app.common.features :as cfeat] [app.common.logging :as l] [app.common.schema :as sm] - [app.common.spec :as us] [app.common.uuid :as uuid] [app.config :as cf] [app.db :as db] @@ -32,16 +31,10 @@ [app.util.services :as sv] [app.util.time :as dt] [app.worker :as wrk] - [clojure.spec.alpha :as s] [cuerdas.core :as str])) ;; --- 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 "select tpr.is_owner, tpr.is_admin, @@ -351,7 +344,7 @@ (def ^:private schema:create-team [:map {:title "create-team"} - [:name :string] + [:name [:string {:max 250}]] [:features {:optional true} ::cfeat/features] [:id {:optional true} ::sm/uuid]]) @@ -438,12 +431,14 @@ ;; --- Mutation: Update Team -(s/def ::update-team - (s/keys :req [::rpc/profile-id] - :req-un [::name ::id])) +(def ^:private schema:update-team + [:map {:title "update-team"} + [:name [:string {:max 250}]] + [:id ::sm/uuid]]) (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}] (db/with-atomic [conn pool] (check-edition-permissions! conn profile-id id) @@ -503,14 +498,14 @@ nil)) -(s/def ::reassign-to ::us/uuid) -(s/def ::leave-team - (s/keys :req [::rpc/profile-id] - :req-un [::id] - :opt-un [::reassign-to])) +(def ^:private schema:leave-team + [:map {:title "leave-team"} + [:id ::sm/uuid] + [:reassign-to {:optional true} ::sm/uuid]]) (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}] (db/with-atomic [conn pool] (leave-team conn (assoc params :profile-id profile-id)))) @@ -539,12 +534,13 @@ :id team-id}) team)) -(s/def ::delete-team - (s/keys :req [::rpc/profile-id] - :req-un [::id])) +(def ^:private schema:delete-team + [:map {:title "delete-team"} + [:id ::sm/uuid]]) (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}] (db/with-atomic [conn pool] (let [perms (get-permissions conn profile-id id)] @@ -557,10 +553,6 @@ ;; --- 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 ;; https://tree.taiga.io/project/penpot/issue/1083 (def valid-roles @@ -624,25 +616,29 @@ :profile-id member-id}) nil))) -(s/def ::update-team-member-role - (s/keys :req [::rpc/profile-id] - :req-un [::team-id ::member-id ::role])) +(def ^:private schema:update-team-member-role + [:map {:title "update-team-member-role"} + [:team-id ::sm/uuid] + [:member-id ::sm/uuid] + [:role schema: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}] (db/with-atomic [conn pool] (update-team-member-role conn (assoc params :profile-id profile-id)))) - ;; --- Mutation: Delete Team Member -(s/def ::delete-team-member - (s/keys :req [::rpc/profile-id] - :req-un [::team-id ::member-id])) +(def ^:private schema:delete-team-member + [:map {:title "delete-team-member"} + [:team-id ::sm/uuid] + [:member-id ::sm/uuid]]) (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}] (db/with-atomic [conn pool] (let [perms (get-permissions conn profile-id team-id)] @@ -665,13 +661,14 @@ (declare upload-photo) (declare ^:private update-team-photo) -(s/def ::file ::media/upload) -(s/def ::update-team-photo - (s/keys :req [::rpc/profile-id] - :req-un [::team-id ::file])) +(def ^:private schema:update-team-photo + [:map {:title "update-team-photo"} + [:team-id ::sm/uuid] + [:file ::media/upload]]) (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}] ;; Validate incoming mime type (media/validate-media-type! file #{"image/jpeg" "image/png" "image/webp"}) @@ -809,7 +806,7 @@ (def ^:private schema:create-team-invitations [:map {:title "create-team-invitations"} [:team-id ::sm/uuid] - [:role [::sm/one-of #{:owner :admin :editor}]] + [:role schema:role] [:emails ::sm/set-of-emails]]) (sv/defmethod ::create-team-invitations @@ -866,12 +863,6 @@ ;; --- 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 [:map {:title "create-team-with-invitations"} [:name :string] @@ -930,12 +921,14 @@ ;; --- Query: get-team-invitation-token -(s/def ::get-team-invitation-token - (s/keys :req [::rpc/profile-id] - :req-un [::team-id ::email])) +(def ^:private schema:get-team-invitation-token + [:map {:title "get-team-invitation-token"} + [:team-id ::sm/uuid] + [:email ::sm/email]]) (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}] (check-read-permissions! pool profile-id team-id) (let [email (profile/clean-email email) @@ -956,12 +949,15 @@ ;; --- Mutation: Update invitation role -(s/def ::update-team-invitation-role - (s/keys :req [::rpc/profile-id] - :req-un [::team-id ::email ::role])) +(def ^:private schema:update-team-invitation-role + [:map {:title "update-team-invitation-role"} + [:team-id ::sm/uuid] + [:email ::sm/email] + [:role schema: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}] (db/with-atomic [conn pool] (let [perms (get-permissions conn profile-id team-id)] @@ -977,12 +973,14 @@ ;; --- Mutation: Delete invitation -(s/def ::delete-team-invitation - (s/keys :req [::rpc/profile-id] - :req-un [::team-id ::email])) +(def ^:private schema:delete-team-invition + [:map {:title "delete-team-invitation"} + [:team-id ::sm/uuid] + [:email ::sm/email]]) (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}] (db/with-atomic [conn pool] (let [perms (get-permissions conn profile-id team-id)] diff --git a/backend/src/app/tokens.clj b/backend/src/app/tokens.clj index 30ca32b3b..60b0d50b2 100644 --- a/backend/src/app/tokens.clj +++ b/backend/src/app/tokens.clj @@ -8,18 +8,19 @@ "Tokens generation API." (:require [app.common.data :as d] + [app.common.data.macros :as dm] [app.common.exceptions :as ex] - [app.common.spec :as us] [app.common.transit :as t] [app.util.time :as dt] - [buddy.sign.jwe :as jwe] - [clojure.spec.alpha :as s])) - -(s/def ::tokens-key bytes?) + [buddy.sign.jwe :as jwe])) (defn generate [{: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 (assoc :iat (dt/now)) (d/without-nils) @@ -39,15 +40,13 @@ (ex/raise :type :validation :code :invalid-token :reason :token-expired - :params params - :claims claims)) + :params params)) (when (and (contains? params :iss) (not= (:iss claims) (:iss params))) (ex/raise :type :validation :code :invalid-token :reason :invalid-issuer - :claims claims :params params)) claims)) diff --git a/common/src/app/common/data.cljc b/common/src/app/common/data.cljc index 736803631..645091fd0 100644 --- a/common/src/app/common/data.cljc +++ b/common/src/app/common/data.cljc @@ -224,7 +224,6 @@ [coll] (into [] (remove nil?) coll)) - (defn without-nils "Given a map, return a map removing key-value pairs when value is `nil`." diff --git a/common/src/app/common/geom/modifiers.cljc b/common/src/app/common/geom/modifiers.cljc index 813fd784a..ec4646f66 100644 --- a/common/src/app/common/geom/modifiers.cljc +++ b/common/src/app/common/geom/modifiers.cljc @@ -269,6 +269,13 @@ (keep (mk-check-auto-layout objects)) 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 "Recalculates the layouts to adjust the sizing: auto new sizes" [modif-tree sizing-auto-layouts objects bounds ignore-constraints] @@ -286,7 +293,7 @@ (d/seek sizing-auto-layouts)) shapes - (if from-layout + (if (and from-layout (not (full-tree? objects from-layout))) (cgst/resolve-subtree from-layout layout-id objects) (cgst/resolve-tree #{layout-id} objects)) diff --git a/common/src/app/common/types/component.cljc b/common/src/app/common/types/component.cljc index 95bf3016a..bcd6ef3b3 100644 --- a/common/src/app/common/types/component.cljc +++ b/common/src/app/common/types/component.cljc @@ -189,14 +189,20 @@ (when 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 "If the shape has a :touched group in the form :swap-slot-, get the id." [shape] - (let [group (->> (:touched shape) - (map name) - (d/seek #(str/starts-with? % "swap-slot-")))] + (let [group (d/seek swap-slot? (:touched shape))] (when group - (uuid/uuid (subs group 10))))) + (group->swap-slot group)))) (defn match-swap-slot? [shape-main shape-inst] @@ -264,3 +270,16 @@ ;; Non instance, non copy. We allow (or (not (instance-head? shape)) (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))) \ No newline at end of file diff --git a/common/test/common_tests/types/types_component_test.cljc b/common/test/common_tests/types/types_component_test.cljc new file mode 100644 index 000000000..cff174329 --- /dev/null +++ b/common/test/common_tests/types/types_component_test.cljc @@ -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)))))) diff --git a/frontend/playwright/data/viewer/get-file-fragment-single-board.json b/frontend/playwright/data/viewer/get-file-fragment-single-board.json new file mode 100644 index 000000000..8c1e62a15 --- /dev/null +++ b/frontend/playwright/data/viewer/get-file-fragment-single-board.json @@ -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" + } +} \ No newline at end of file diff --git a/frontend/playwright/data/viewer/get-view-only-bundle-single-board.json b/frontend/playwright/data/viewer/get-view-only-bundle-single-board.json new file mode 100644 index 000000000..9284de685 --- /dev/null +++ b/frontend/playwright/data/viewer/get-view-only-bundle-single-board.json @@ -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 + } +} \ No newline at end of file diff --git a/frontend/playwright/ui/pages/ViewerPage.js b/frontend/playwright/ui/pages/ViewerPage.js index cd328eb65..18c8cebe9 100644 --- a/frontend/playwright/ui/pages/ViewerPage.js +++ b/frontend/playwright/ui/pages/ViewerPage.js @@ -33,10 +33,8 @@ export class ViewerPage extends BaseWebSocketPage { super(page); } - async goToViewer() { - await this.page.goto( - `/#/view/${ViewerPage.anyFileId}?page-id=${ViewerPage.anyPageId}§ion=interactions&index=0`, - ); + async goToViewer({ fileId = ViewerPage.anyFileId, pageId = ViewerPage.anyPageId } = {}) { + await this.page.goto(`/#/view/${fileId}?page-id=${pageId}§ion=interactions&index=0`); this.#ws = await this.waitForNotificationsWebSocket(); await this.#ws.mockOpen(); diff --git a/frontend/playwright/ui/pages/WorkspacePage.js b/frontend/playwright/ui/pages/WorkspacePage.js index 9a3e83716..4c82e0155 100644 --- a/frontend/playwright/ui/pages/WorkspacePage.js +++ b/frontend/playwright/ui/pages/WorkspacePage.js @@ -111,7 +111,7 @@ export class WorkspacePage extends BaseWebSocketPage { const layer = this.layers.getByTestId("layer-item").filter({ has: this.page.getByText(name) }); await layer.getByRole("button").click(clickOptions); } - + async clickAssets(clickOptions = {}) { await this.assets.click(clickOptions); } diff --git a/frontend/playwright/ui/specs/viewer-header.spec.js b/frontend/playwright/ui/specs/viewer-header.spec.js index 01cfb8634..48f282965 100644 --- a/frontend/playwright/ui/specs/viewer-header.spec.js +++ b/frontend/playwright/ui/specs/viewer-header.spec.js @@ -5,6 +5,18 @@ test.beforeEach(async ({ 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 }) => { const viewerPage = new ViewerPage(page); 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 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/); +}); diff --git a/frontend/src/app/main/data/changes.cljs b/frontend/src/app/main/data/changes.cljs index 4da8d30da..d16cfafc5 100644 --- a/frontend/src/app/main/data/changes.cljs +++ b/frontend/src/app/main/data/changes.cljs @@ -69,9 +69,10 @@ (cpc/check-changes! undo-changes))) (let [commit-id (or commit-id (uuid/next)) + source (d/nilv source :local) commit {:id commit-id :created-at (dt/now) - :source (d/nilv source :local) + :source source :origin (ptk/type origin) :features features :file-id file-id @@ -110,9 +111,7 @@ redo-changes (if pending (into redo-changes - (comp - (map :redo-changes) - (mapcat identity)) + (mapcat :redo-changes) pending) redo-changes)] diff --git a/frontend/src/app/main/data/persistence.cljs b/frontend/src/app/main/data/persistence.cljs index 8fbcc372a..08a0719b3 100644 --- a/frontend/src/app/main/data/persistence.cljs +++ b/frontend/src/app/main/data/persistence.cljs @@ -182,7 +182,7 @@ (log/debug :hint "initialize persistence") (let [stoper-s (rx/filter (ptk/type? ::initialize-persistence) stream) - commits-s + local-commits-s (->> stream (rx/filter dch/commit?) (rx/map deref) @@ -192,28 +192,34 @@ notifier-s (rx/merge - (->> commits-s + (->> local-commits-s (rx/debounce 3000) (rx/tap #(log/trc :hint "persistence beat"))) (->> stream (rx/filter #(= % ::force-persist))))] (rx/merge - (->> commits-s + (->> local-commits-s (rx/debounce 200) (rx/map (fn [_] (update-status :pending))) (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 ;; chunks (very near in time commits) and append them to the ;; persistence queue - (->> commits-s + (->> local-commits-s (rx/buffer-until notifier-s) (rx/mapcat merge-commit) - (rx/mapcat (fn [commit] - (rx/of (append-commit commit) - (dch/update-indexes commit)))) + (rx/map append-commit) (rx/take-until (rx/delay 100 stoper-s)) (rx/finalize (fn [] (log/debug :hint "finalize persistence: changes watcher")))) diff --git a/frontend/src/app/main/data/viewer.cljs b/frontend/src/app/main/data/viewer.cljs index a45e75939..456413018 100644 --- a/frontend/src/app/main/data/viewer.cljs +++ b/frontend/src/app/main/data/viewer.cljs @@ -253,6 +253,18 @@ ;; --- 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 (ptk/reify ::increase-zoom ptk/UpdateEvent @@ -293,7 +305,10 @@ minzoom (min wdiff hdiff)] (-> state (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 (ptk/reify ::zoom-to-fill @@ -309,7 +324,9 @@ maxzoom (max wdiff hdiff)] (-> state (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 (ptk/reify ::toggle-zoom-style diff --git a/frontend/src/app/main/data/workspace/transforms.cljs b/frontend/src/app/main/data/workspace/transforms.cljs index 7fe33314d..abb7a7c83 100644 --- a/frontend/src/app/main/data/workspace/transforms.cljs +++ b/frontend/src/app/main/data/workspace/transforms.cljs @@ -431,7 +431,7 @@ (watch [_ state stream] (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) ;; We toggle the selection so we don't have to wait for the event diff --git a/frontend/src/app/main/render.cljs b/frontend/src/app/main/render.cljs index 16c961b5d..bbf85e4ad 100644 --- a/frontend/src/app/main/render.cljs +++ b/frontend/src/app/main/render.cljs @@ -149,7 +149,7 @@ 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)) 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} svg-raw? (= :svg-raw (:type shape))] (if-not svg-raw? diff --git a/frontend/src/app/main/ui/shapes/export.cljs b/frontend/src/app/main/ui/shapes/export.cljs index 8ac2387e8..35895b777 100644 --- a/frontend/src/app/main/ui/shapes/export.cljs +++ b/frontend/src/app/main/ui/shapes/export.cljs @@ -50,6 +50,9 @@ (defn bool->str [val] (when (some? val) (str val))) +(defn touched->str [val] + (str/join " " (map str val))) + (defn add-factory [shape] (fn add! ([props attr] @@ -136,7 +139,6 @@ (cond-> bool? (add! :bool-type))))) - (defn add-library-refs [props shape] (let [add! (add-factory shape)] (-> props @@ -150,7 +152,8 @@ (add! :component-id) (add! :component-root) (add! :main-instance) - (add! :shape-ref)))) + (add! :shape-ref) + (add! :touched touched->str)))) (defn prefix-keys [m] (letfn [(prefix-entry [[k v]] diff --git a/frontend/src/app/main/ui/shapes/shape.cljs b/frontend/src/app/main/ui/shapes/shape.cljs index e0ab37c45..e4082b89c 100644 --- a/frontend/src/app/main/ui/shapes/shape.cljs +++ b/frontend/src/app/main/ui/shapes/shape.cljs @@ -72,6 +72,8 @@ (obj/set! "pointerEvents" pointer-events) (cond-> (not (cfh/frame-shape? shape)) (obj/set! "opacity" (:opacity shape))) + (cond-> (:hidden shape) + (obj/set! "display" "none")) (cond-> (and blend-mode (not= blend-mode :normal)) (obj/set! "mixBlendMode" (d/name blend-mode)))) diff --git a/frontend/src/app/main/ui/viewer/header.cljs b/frontend/src/app/main/ui/viewer/header.cljs index 03dd06548..603c1cba1 100644 --- a/frontend/src/app/main/ui/viewer/header.cljs +++ b/frontend/src/app/main/ui/viewer/header.cljs @@ -138,7 +138,7 @@ (mf/deps page) (fn [] (modal/show! :share-link {:page page :file file}) - (modal/allow-click-outside!))) + (modal/disallow-click-outside!))) handle-increase (mf/use-fn diff --git a/frontend/src/app/main/ui/workspace/sidebar/assets/common.cljs b/frontend/src/app/main/ui/workspace/sidebar/assets/common.cljs index d99e9dcac..e3a1cf372 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/assets/common.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/assets/common.cljs @@ -402,9 +402,11 @@ (st/emit! (dwl/nav-to-component-file library-id comp)))) do-show-component - #(if local-component? - (do-show-local-component) - (do-show-remote-component)) + (fn [] + (st/emit! dw/hide-context-menu) + (if local-component? + (do-show-local-component) + (do-show-remote-component))) do-restore-component #(let [;; Extract a map of component-id -> component-file in order to avoid duplicates diff --git a/frontend/src/app/util/mouse.cljs b/frontend/src/app/util/mouse.cljs index 4576ed325..8f3e7652a 100644 --- a/frontend/src/app/util/mouse.cljs +++ b/frontend/src/app/util/mouse.cljs @@ -72,12 +72,20 @@ (defn drag-stopper "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." - [stream] - (rx/merge - (->> stream - (rx/filter blur-event?)) - (->> stream - (rx/filter mouse-event?) - (rx/filter mouse-up-event?)) - (->> stream - (rx/filter #(= % :interrupt))))) + ([stream] + (drag-stopper stream nil)) + ([stream {:keys [blur? up-mouse? interrupt?] :or {blur? true up-mouse? true interrupt? true}}] + (rx/merge + (if blur? + (->> stream + (rx/filter blur-event?)) + (rx/empty)) + (if up-mouse? + (->> stream + (rx/filter mouse-event?) + (rx/filter mouse-up-event?)) + (rx/empty)) + (if interrupt? + (->> stream + (rx/filter #(= % :interrupt))) + (rx/empty))))) diff --git a/frontend/src/app/worker/import/parser.cljs b/frontend/src/app/worker/import/parser.cljs index fab4075ca..5eda72daa 100644 --- a/frontend/src/app/worker/import/parser.cljs +++ b/frontend/src/app/worker/import/parser.cljs @@ -12,6 +12,7 @@ [app.common.geom.matrix :as gmt] [app.common.geom.point :as gpt] [app.common.svg.path :as svg.path] + [app.common.types.component :as ctk] [app.common.types.shape.interactions :as ctsi] [app.common.uuid :as uuid] [app.util.json :as json] @@ -129,6 +130,15 @@ (into {})) 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 [m attrs] (reduce-kv @@ -424,7 +434,8 @@ component-file (get-meta node :component-file uuid/uuid) shape-ref (get-meta node :shape-ref uuid/uuid) 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 (some? stroke-color-ref-id) @@ -442,7 +453,10 @@ (assoc :main-instance main-instance?) (some? shape-ref) - (assoc :shape-ref shape-ref)))) + (assoc :shape-ref shape-ref) + + (seq touched) + (assoc :touched touched)))) (defn add-fill [props node svg-data]