0
Fork 0
mirror of https://github.com/penpot/penpot.git synced 2025-03-27 23:21:47 -05:00

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

This commit is contained in:
Andrey Antukh 2023-07-13 12:13:06 +02:00
commit 8eb64de062
43 changed files with 827 additions and 532 deletions

View file

@ -70,7 +70,25 @@
- Fix focus handling on comments edition [Taiga #5560](https://tree.taiga.io/project/penpot/issue/5560)
- Fix incorrect fullname use on registring user after OIDC authentication [Taiga #5517](https://tree.taiga.io/project/penpot/issue/5517)
- Fix incorrect modified-at on project after import file [Taiga #5268](https://tree.taiga.io/project/penpot/issue/5268)
- Fix incorrect message after sending invitation to already member [Taiga 5599](https://tree.taiga.io/project/penpot/issue/5599)
- Fix text decoration on button [Taiga #5301](https://tree.taiga.io/project/penpot/issue/5301)
- Fix menu order on design tab [Taiga #5195](https://tree.taiga.io/project/penpot/issue/5195)
- Fix search bar width on layer tab [Taiga #5445](https://tree.taiga.io/project/penpot/issue/5445)
- Fix border radius values with decimals [Taiga #5283](https://tree.taiga.io/project/penpot/issue/5283)
- Fix shortcuts translations not homogenized [Taiga #5141](https://tree.taiga.io/project/penpot/issue/5141)
- Fix overlay manual position in nested boards [Taiga #5135](https://tree.taiga.io/project/penpot/issue/5135)
- Fix close overlay from a nested board [Taiga #5587](https://tree.taiga.io/project/penpot/issue/5587)
- Fix overlay position when it has shadow or blur [Taiga #4752](https://tree.taiga.io/project/penpot/issue/4752)
- Fix overlay position when there are elements fixed when scrolling [Taiga #4383](https://tree.taiga.io/project/penpot/issue/4383)
- Fix problem when sliding color picker in selected-colors [#3150](https://github.com/penpot/penpot/issues/3150)
- Fix error screen on upload image error [Taiga #5608](https://tree.taiga.io/project/penpot/issue/5608)
- Fix bad frame-id for certain componentes [#3205](https://github.com/penpot/penpot/issues/3205)
- Fix paste elements at bottom of frame [Taig #5253](https://tree.taiga.io/project/penpot/issue/5253)
- Fix new-file button on project not redirecting to the new file [Taiga #5610](https://tree.taiga.io/project/penpot/issue/5610)
- Fix retrieve user comments in dashboard [Taiga #5607](https://tree.taiga.io/project/penpot/issue/5607)
- Locks shapes when moved inside a locked parent [Taiga #5252](https://tree.taiga.io/project/penpot/issue/5252)
- Fix rotate several elements in bulk [Taiga #5165](https://tree.taiga.io/project/penpot/issue/5165)
### :arrow_up: Deps updates
- Update google fonts catalog (at 2023/07/06) [Taiga #5592](https://tree.taiga.io/project/penpot/issue/5592)

View file

@ -10,6 +10,7 @@
[app.common.data.macros :as dm]
[app.common.exceptions :as ex]
[app.common.logging :as l]
[app.common.schema :as sm]
[app.common.spec :as us]
[app.common.uuid :as uuid]
[app.config :as cf]
@ -719,29 +720,22 @@
itoken))))
(s/def ::email ::us/email)
(s/def ::emails ::us/set-of-valid-emails)
(s/def ::create-team-invitations
(s/keys :req [::rpc/profile-id]
:req-un [::team-id ::role]
:opt-un [::email ::emails]))
(def ^:private schema:create-team-invitations
[:map {:title "create-team-invitations"}
[:team-id ::sm/uuid]
[:role [::sm/one-of #{:owner :admin :editor}]]
[:emails ::sm/set-of-emails]])
(sv/defmethod ::create-team-invitations
"A rpc call that allow to send a single or multiple invitations to
join the team."
{::doc/added "1.17"}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id team-id email emails role] :as params}]
{::doc/added "1.17"
::sm/params schema:create-team-invitations}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id team-id emails role] :as params}]
(db/with-atomic [conn pool]
(let [perms (get-permissions conn profile-id team-id)
profile (db/get-by-id conn :profile profile-id)
team (db/get-by-id conn :team team-id)
;; Members emails. We don't re-send inviation to already existing members
member? (into #{}
(map :email)
(db/exec! conn [sql:team-members team-id]))
emails (cond-> (or emails #{}) (string? email) (conj email))]
team (db/get-by-id conn :team team-id)]
(run! (partial quotes/check-quote! conn)
(list {::quotes/id ::quotes/invitations-per-team
@ -764,9 +758,13 @@
:hint "looks like the profile has reported repeatedly as spam or has permanent bounces"))
(let [cfg (assoc cfg ::db/conn conn)
invitations (into []
members (->> (db/exec! conn [sql:team-members team-id])
(into #{} (map :email)))
invitations (into #{}
(comp
(remove member?)
;; We don't re-send inviation to already existing members
(remove (partial contains? members))
(map (fn [email]
{:email (str/lower email)
:team team
@ -774,7 +772,8 @@
:role role}))
(keep (partial create-invitation cfg)))
emails)]
(with-meta invitations
(with-meta {:total (count invitations)
:invitations invitations}
{::audit/props {:invitations (count invitations)}})))))

View file

@ -37,7 +37,7 @@
:role :editor}]
;; invite external user without complaints
(let [data (assoc data :email "foo@bar.com")
(let [data (assoc data :emails ["foo@bar.com"])
out (th/command! data)
;; retrieve the value from the database and check its content
invitation (db/exec-one!
@ -52,7 +52,7 @@
;; invite internal user without complaints
(th/reset-mock! mock)
(let [data (assoc data :email (:email profile2))
(let [data (assoc data :emails [(:email profile2)])
out (th/command! data)]
(t/is (th/success? out))
(t/is (= 1 (:call-count (deref mock)))))
@ -60,7 +60,7 @@
;; invite user with complaint
(th/create-global-complaint-for pool {:type :complaint :email "foo@bar.com"})
(th/reset-mock! mock)
(let [data (assoc data :email "foo@bar.com")
(let [data (assoc data :emails ["foo@bar.com"])
out (th/command! data)]
(t/is (th/success? out))
(t/is (= 1 (:call-count (deref mock)))))
@ -79,7 +79,7 @@
(th/reset-mock! mock)
(th/create-global-complaint-for pool {:type :bounce :email "foo@bar.com"})
(let [data (assoc data :email "foo@bar.com")
(let [data (assoc data :emails ["foo@bar.com"])
out (th/command! data)]
(t/is (not (th/success? out)))
@ -92,7 +92,7 @@
;; invite internal user that is muted
(th/reset-mock! mock)
(let [data (assoc data :email (:email profile3))
(let [data (assoc data :emails [(:email profile3)])
out (th/command! data)]
(t/is (not (th/success? out)))
@ -118,7 +118,7 @@
;; Try to invite a not existing user
(let [data {::th/type :create-team-invitations
::rpc/profile-id (:id profile1)
:email "notexisting@example.com"
:emails ["notexisting@example.com"]
:team-id (:id team)
:role :editor}
out (th/command! data)]
@ -126,15 +126,15 @@
;; (th/print-result! out)
(t/is (th/success? out))
(t/is (= 1 (:call-count @mock)))
(t/is (= 1 (-> out :result count)))
(t/is (= 1 (-> out :result :total)))
(let [token (-> out :result first)
(let [token (-> out :result :invitations first)
claims (tokens/decode sprops token)]
(t/is (= :team-invitation (:iss claims)))
(t/is (= (:id profile1) (:profile-id claims)))
(t/is (= :editor (:role claims)))
(t/is (= (:id team) (:team-id claims)))
(t/is (= (:email data) (:member-email claims)))
(t/is (= (first (:emails data)) (:member-email claims)))
(t/is (nil? (:member-id claims)))))
(th/reset-mock! mock)
@ -142,7 +142,7 @@
;; Try to invite existing user
(let [data {::th/type :create-team-invitations
::rpc/profile-id (:id profile1)
:email (:email profile2)
:emails [(:email profile2)]
:team-id (:id team)
:role :editor}
out (th/command! data)]
@ -150,15 +150,15 @@
;; (th/print-result! out)
(t/is (th/success? out))
(t/is (= 1 (:call-count @mock)))
(t/is (= 1 (-> out :result count)))
(t/is (= 1 (-> out :result :total)))
(let [token (-> out :result first)
(let [token (-> out :result :invitations first)
claims (tokens/decode sprops token)]
(t/is (= :team-invitation (:iss claims)))
(t/is (= (:id profile1) (:profile-id claims)))
(t/is (= :editor (:role claims)))
(t/is (= (:id team) (:team-id claims)))
(t/is (= (:email data) (:member-email claims)))
(t/is (= (first (:emails data)) (:member-email claims)))
(t/is (= (:id profile2) (:member-id claims)))))
)))
@ -264,7 +264,7 @@
;; invite internal user without complaints
(with-redefs [app.config/flags #{}]
(th/reset-mock! mock)
(let [data (assoc data :email (:email profile2))
(let [data (assoc data :emails [(:email profile2)])
out (th/command! data)]
(t/is (th/success? out))
(t/is (= 0 (:call-count (deref mock)))))

View file

@ -280,13 +280,15 @@
:else
(let [objects (lookup-objects file)
bool-content (gsh/calc-bool-content bool objects)
bool' (gsh/update-bool-selrect bool children objects)]
(commit-change
file
{:type :mod-obj
:id bool-id
:operations
[{:type :set :attr :selrect :val (:selrect bool') :ignore-touched true}
[{:type :set :attr :bool-content :val bool-content :ignore-touched true}
{:type :set :attr :selrect :val (:selrect bool') :ignore-touched true}
{:type :set :attr :points :val (:points bool') :ignore-touched true}
{:type :set :attr :x :val (-> bool' :selrect :x) :ignore-touched true}
{:type :set :attr :y :val (-> bool' :selrect :y) :ignore-touched true}

View file

@ -6,4 +6,4 @@
(ns app.common.files.defaults)
(def version 23)
(def version 24)

View file

@ -436,32 +436,6 @@
(update :pages-index update-vals update-container)
(update :components update-vals update-container))))
(defmethod migrate 20
[data]
(letfn [(update-object [objects object]
(let [frame-id (:frame-id object)
calculated-frame-id
(or (->> (cph/get-parent-ids objects (:id object))
(map (d/getf objects))
(d/seek cph/frame-shape?)
:id)
;; If we cannot find any we let the frame-id as it was before
frame-id)]
(when (not= frame-id calculated-frame-id)
(log/info :hint "Fix wrong frame-id"
:shape (:name object)
:id (:id object)
:current (dm/get-in objects [frame-id :name])
:calculated (get-in objects [calculated-frame-id :name])))
(assoc object :frame-id calculated-frame-id)))
(update-container [container]
(update container :objects #(update-vals % (partial update-object %))))]
(-> data
(update :pages-index update-vals update-container)
(update :components update-vals update-container))))
(defmethod migrate 21
[data]
(letfn [(update-object [object]
@ -517,3 +491,29 @@
(-> data
(update :pages-index update-vals update-container)
(update :components update-vals update-container))))
(defmethod migrate 24
[data]
(letfn [(update-object [objects object]
(let [frame-id (:frame-id object)
calculated-frame-id
(or (->> (cph/get-parent-ids objects (:id object))
(map (d/getf objects))
(d/seek cph/frame-shape?)
:id)
;; If we cannot find any we let the frame-id as it was before
frame-id)]
(when (not= frame-id calculated-frame-id)
(log/info :hint "Fix wrong frame-id"
:shape (:name object)
:id (:id object)
:current (dm/get-in objects [frame-id :name])
:calculated (get-in objects [calculated-frame-id :name])))
(assoc object :frame-id calculated-frame-id)))
(update-container [container]
(update container :objects #(update-vals % (partial update-object %))))]
(-> data
(update :pages-index update-vals update-container)
(update :components update-vals update-container))))

View file

@ -163,6 +163,7 @@
:else
(cph/reduce-objects
objects
(fn [shape]
(and (d/not-empty? (:shapes shape))
(or (not (cph/frame-shape? shape))

View file

@ -406,7 +406,7 @@
(update page :objects update-vals root-to-board))]
(-> file-data
(add-instance-grid (sort-by :name components))
(add-instance-grid (reverse (sort-by :name components)))
(update :pages-index update-vals roots-to-board)
(assoc-in [:options :components-v2] true))))))))

View file

@ -495,8 +495,7 @@
"expected compatible interaction map"
(has-overlay-opts interaction))
(let [
;; When the interactive item is inside a nested frame we need to add to the offset the position
(let [;; When the interactive item is inside a nested frame we need to add to the offset the position
;; of the parent-frame otherwise the position won't match
shape-frame (cph/get-frame objects shape)
@ -505,10 +504,10 @@
(cph/root-frame? shape-frame)
(cph/root? shape-frame))
frame-offset
(gpt/add frame-offset (gpt/point shape-frame)))
]
(gpt/add frame-offset (gpt/point shape-frame)))]
(if (nil? dest-frame)
(gpt/point 0 0)
[(gpt/point 0 0) [:top :left]]
(let [overlay-size (gsb/get-object-bounds objects dest-frame)
base-frame-size (:selrect base-frame)
relative-to-shape-size (:selrect relative-to-shape)
@ -526,37 +525,46 @@
overlay-position
{:x (- (:x overlay-position) (:x relative-to-adjusted-to-base-frame))
:y (- (:y overlay-position) (:y relative-to-adjusted-to-base-frame))})]
(case (:overlay-pos-type interaction)
:center
(gpt/point (+ (:x base-position) (/ (- (:width relative-to-shape-size) (:width overlay-size)) 2))
(+ (:y base-position) (/ (- (:height relative-to-shape-size) (:height overlay-size)) 2)))
[(gpt/point (+ (:x base-position) (/ (- (:width relative-to-shape-size) (:width overlay-size)) 2))
(+ (:y base-position) (/ (- (:height relative-to-shape-size) (:height overlay-size)) 2)))
[:center :center]]
:top-left
(gpt/point (:x base-position) (:y base-position))
[(gpt/point (:x base-position) (:y base-position))
[:top :left]]
:top-right
(gpt/point (+ (:x base-position) (- (:width relative-to-shape-size) (:width overlay-size)))
(:y base-position))
[(gpt/point (+ (:x base-position) (- (:width relative-to-shape-size) (:width overlay-size)))
(:y base-position))
[:top :right]]
:top-center
(gpt/point (+ (:x base-position) (/ (- (:width relative-to-shape-size) (:width overlay-size)) 2))
(:y base-position))
[(gpt/point (+ (:x base-position) (/ (- (:width relative-to-shape-size) (:width overlay-size)) 2))
(:y base-position))
[:top :center]]
:bottom-left
(gpt/point (:x base-position)
(+ (:y base-position) (- (:height relative-to-shape-size) (:height overlay-size))))
[(gpt/point (:x base-position)
(+ (:y base-position) (- (:height relative-to-shape-size) (:height overlay-size))))
[:bottom :left]]
:bottom-right
(gpt/point (+ (:x base-position) (- (:width relative-to-shape-size) (:width overlay-size)))
(+ (:y base-position) (- (:height relative-to-shape-size) (:height overlay-size))))
[(gpt/point (+ (:x base-position) (- (:width relative-to-shape-size) (:width overlay-size)))
(+ (:y base-position) (- (:height relative-to-shape-size) (:height overlay-size))))
[:bottom :right]]
:bottom-center
(gpt/point (+ (:x base-position) (/ (- (:width relative-to-shape-size) (:width overlay-size)) 2))
(+ (:y base-position) (- (:height relative-to-shape-size) (:height overlay-size))))
[(gpt/point (+ (:x base-position) (/ (- (:width relative-to-shape-size) (:width overlay-size)) 2))
(+ (:y base-position) (- (:height relative-to-shape-size) (:height overlay-size))))
[:bottom :center]]
:manual
(gpt/point (+ (:x base-position) (:x overlay-position))
(+ (:y base-position) (:y overlay-position))))))))
[(gpt/point (+ (:x base-position) (:x overlay-position))
(+ (:y base-position) (:y overlay-position)))
[:top :left]])))))
(defn has-animation?
[interaction]

View file

@ -323,209 +323,275 @@
interaction-rect (ctsi/set-position-relative-to interaction (:id rect))]
(t/testing "Overlay top-left relative to auto"
(let [i2 (ctsi/set-overlay-pos-type interaction-auto :top-left base-frame objects)
overlay-pos (ctsi/calc-overlay-position i2 rect objects base-frame base-frame overlay-frame frame-offset)]
[overlay-pos [snap-v snap-h]] (ctsi/calc-overlay-position i2 rect objects base-frame base-frame overlay-frame frame-offset)]
(t/is (= (:x overlay-pos) 0))
(t/is (= (:y overlay-pos) 0))))
(t/is (= (:y overlay-pos) 0))
(t/is (= snap-v :top))
(t/is (= snap-h :left))))
(t/testing "Overlay top-center relative to auto"
(let [i2 (ctsi/set-overlay-pos-type interaction-auto :top-center base-frame objects)
overlay-pos (ctsi/calc-overlay-position i2 rect objects base-frame base-frame overlay-frame frame-offset)]
[overlay-pos [snap-v snap-h]] (ctsi/calc-overlay-position i2 rect objects base-frame base-frame overlay-frame frame-offset)]
(t/is (= (:x overlay-pos) 35))
(t/is (= (:y overlay-pos) 0))))
(t/is (= (:y overlay-pos) 0))
(t/is (= snap-v :top))
(t/is (= snap-h :center))))
(t/testing "Overlay top-right relative to auto"
(let [i2 (ctsi/set-overlay-pos-type interaction-auto :top-right base-frame objects)
overlay-pos (ctsi/calc-overlay-position i2 rect objects base-frame base-frame overlay-frame frame-offset)]
[overlay-pos [snap-v snap-h]] (ctsi/calc-overlay-position i2 rect objects base-frame base-frame overlay-frame frame-offset)]
(t/is (= (:x overlay-pos) 70))
(t/is (= (:y overlay-pos) 0))))
(t/is (= (:y overlay-pos) 0))
(t/is (= snap-v :top))
(t/is (= snap-h :right))))
(t/testing "Overlay bottom-left relative to auto"
(let [i2 (ctsi/set-overlay-pos-type interaction-auto :bottom-left base-frame objects)
overlay-pos (ctsi/calc-overlay-position i2 rect objects base-frame base-frame overlay-frame frame-offset)]
[overlay-pos [snap-v snap-h]] (ctsi/calc-overlay-position i2 rect objects base-frame base-frame overlay-frame frame-offset)]
(t/is (= (:x overlay-pos) 0))
(t/is (= (:y overlay-pos) 80))))
(t/is (= (:y overlay-pos) 80))
(t/is (= snap-v :bottom))
(t/is (= snap-h :left))))
(t/testing "Overlay bottom-center relative to auto"
(let [i2 (ctsi/set-overlay-pos-type interaction-auto :bottom-center base-frame objects)
overlay-pos (ctsi/calc-overlay-position i2 rect objects base-frame base-frame overlay-frame frame-offset)]
[overlay-pos [snap-v snap-h]] (ctsi/calc-overlay-position i2 rect objects base-frame base-frame overlay-frame frame-offset)]
(t/is (= (:x overlay-pos) 35))
(t/is (= (:y overlay-pos) 80))))
(t/is (= (:y overlay-pos) 80))
(t/is (= snap-v :bottom))
(t/is (= snap-h :center))))
(t/testing "Overlay bottom-right relative to auto"
(let [i2 (ctsi/set-overlay-pos-type interaction-auto :bottom-right base-frame objects)
overlay-pos (ctsi/calc-overlay-position i2 rect objects base-frame base-frame overlay-frame frame-offset)]
[overlay-pos [snap-v snap-h]] (ctsi/calc-overlay-position i2 rect objects base-frame base-frame overlay-frame frame-offset)]
(t/is (= (:x overlay-pos) 70))
(t/is (= (:y overlay-pos) 80))))
(t/is (= (:y overlay-pos) 80))
(t/is (= snap-v :bottom))
(t/is (= snap-h :right))))
(t/testing "Overlay center relative to auto"
(let [i2 (ctsi/set-overlay-pos-type interaction-auto :center base-frame objects)
overlay-pos (ctsi/calc-overlay-position i2 rect objects base-frame base-frame overlay-frame frame-offset)]
[overlay-pos [snap-v snap-h]] (ctsi/calc-overlay-position i2 rect objects base-frame base-frame overlay-frame frame-offset)]
(t/is (= (:x overlay-pos) 35))
(t/is (= (:y overlay-pos) 40))))
(t/is (= (:y overlay-pos) 40))
(t/is (= snap-v :center))
(t/is (= snap-h :center))))
(t/testing "Overlay manual relative to auto"
(let [i2 (ctsi/set-overlay-pos-type interaction-auto :center base-frame objects)
overlay-pos (ctsi/calc-overlay-position i2 rect objects base-frame base-frame overlay-frame frame-offset)]
[overlay-pos [snap-v snap-h]] (ctsi/calc-overlay-position i2 rect objects base-frame base-frame overlay-frame frame-offset)]
(t/is (= (:x overlay-pos) 35))
(t/is (= (:y overlay-pos) 40))))
(t/is (= (:y overlay-pos) 40))
(t/is (= snap-v :center))
(t/is (= snap-h :center))))
(t/testing "Overlay manual relative to auto"
(let [i2 (-> interaction-auto
(ctsi/set-overlay-pos-type :manual base-frame objects)
(ctsi/set-overlay-position (gpt/point 12 62)))
overlay-pos (ctsi/calc-overlay-position i2 rect objects base-frame base-frame overlay-frame frame-offset)]
[overlay-pos [snap-v snap-h]] (ctsi/calc-overlay-position i2 rect objects base-frame base-frame overlay-frame frame-offset)]
(t/is (= (:x overlay-pos) 17))
(t/is (= (:y overlay-pos) 67))))
(t/is (= (:y overlay-pos) 67))
(t/is (= snap-v :top))
(t/is (= snap-h :left))))
(t/testing "Overlay top-left relative to base-frame"
(let [i2 (ctsi/set-overlay-pos-type interaction-base-frame :top-left base-frame objects)
overlay-pos (ctsi/calc-overlay-position i2 rect objects base-frame base-frame overlay-frame frame-offset)]
[overlay-pos [snap-v snap-h]] (ctsi/calc-overlay-position i2 rect objects base-frame base-frame overlay-frame frame-offset)]
(t/is (= (:x overlay-pos) 5))
(t/is (= (:y overlay-pos) 5))))
(t/is (= (:y overlay-pos) 5))
(t/is (= snap-v :top))
(t/is (= snap-h :left))))
(t/testing "Overlay top-center relative to base-frame"
(let [i2 (ctsi/set-overlay-pos-type interaction-base-frame :top-center base-frame objects)
overlay-pos (ctsi/calc-overlay-position i2 rect objects base-frame base-frame overlay-frame frame-offset)]
[overlay-pos [snap-v snap-h]] (ctsi/calc-overlay-position i2 rect objects base-frame base-frame overlay-frame frame-offset)]
(t/is (= (:x overlay-pos) 40))
(t/is (= (:y overlay-pos) 5))))
(t/is (= (:y overlay-pos) 5))
(t/is (= snap-v :top))
(t/is (= snap-h :center))))
(t/testing "Overlay top-right relative to base-frame"
(let [i2 (ctsi/set-overlay-pos-type interaction-base-frame :top-right base-frame objects)
overlay-pos (ctsi/calc-overlay-position i2 rect objects base-frame base-frame overlay-frame frame-offset)]
[overlay-pos [snap-v snap-h]] (ctsi/calc-overlay-position i2 rect objects base-frame base-frame overlay-frame frame-offset)]
(t/is (= (:x overlay-pos) 75))
(t/is (= (:y overlay-pos) 5))))
(t/is (= (:y overlay-pos) 5))
(t/is (= snap-v :top))
(t/is (= snap-h :right))))
(t/testing "Overlay bottom-left relative to base-frame"
(let [i2 (ctsi/set-overlay-pos-type interaction-base-frame :bottom-left base-frame objects)
overlay-pos (ctsi/calc-overlay-position i2 rect objects base-frame base-frame overlay-frame frame-offset)]
[overlay-pos [snap-v snap-h]] (ctsi/calc-overlay-position i2 rect objects base-frame base-frame overlay-frame frame-offset)]
(t/is (= (:x overlay-pos) 5))
(t/is (= (:y overlay-pos) 85))))
(t/is (= (:y overlay-pos) 85))
(t/is (= snap-v :bottom))
(t/is (= snap-h :left))))
(t/testing "Overlay bottom-center relative to base-frame"
(let [i2 (ctsi/set-overlay-pos-type interaction-base-frame :bottom-center base-frame objects)
overlay-pos (ctsi/calc-overlay-position i2 rect objects base-frame base-frame overlay-frame frame-offset)]
[overlay-pos [snap-v snap-h]] (ctsi/calc-overlay-position i2 rect objects base-frame base-frame overlay-frame frame-offset)]
(t/is (= (:x overlay-pos) 40))
(t/is (= (:y overlay-pos) 85))))
(t/is (= (:y overlay-pos) 85))
(t/is (= snap-v :bottom))
(t/is (= snap-h :center))))
(t/testing "Overlay bottom-right relative to base-frame"
(let [i2 (ctsi/set-overlay-pos-type interaction-base-frame :bottom-right base-frame objects)
overlay-pos (ctsi/calc-overlay-position i2 rect objects base-frame base-frame overlay-frame frame-offset)]
[overlay-pos [snap-v snap-h]] (ctsi/calc-overlay-position i2 rect objects base-frame base-frame overlay-frame frame-offset)]
(t/is (= (:x overlay-pos) 75))
(t/is (= (:y overlay-pos) 85))))
(t/is (= (:y overlay-pos) 85))
(t/is (= snap-v :bottom))
(t/is (= snap-h :right))))
(t/testing "Overlay center relative to base-frame"
(let [i2 (ctsi/set-overlay-pos-type interaction-base-frame :center base-frame objects)
overlay-pos (ctsi/calc-overlay-position i2 rect objects base-frame base-frame overlay-frame frame-offset)]
[overlay-pos [snap-v snap-h]] (ctsi/calc-overlay-position i2 rect objects base-frame base-frame overlay-frame frame-offset)]
(t/is (= (:x overlay-pos) 40))
(t/is (= (:y overlay-pos) 45))))
(t/is (= (:y overlay-pos) 45))
(t/is (= snap-v :center))
(t/is (= snap-h :center))))
(t/testing "Overlay manual relative to base-frame"
(let [i2 (-> interaction-base-frame
(ctsi/set-overlay-pos-type :manual base-frame objects)
(ctsi/set-overlay-position (gpt/point 12 62)))
overlay-pos (ctsi/calc-overlay-position i2 rect objects base-frame base-frame overlay-frame frame-offset)]
[overlay-pos [snap-v snap-h]] (ctsi/calc-overlay-position i2 rect objects base-frame base-frame overlay-frame frame-offset)]
(t/is (= (:x overlay-pos) 17))
(t/is (= (:y overlay-pos) 67))))
(t/is (= (:y overlay-pos) 67))
(t/is (= snap-v :top))
(t/is (= snap-h :left))))
(t/testing "Overlay top-left relative to popup"
(let [i2 (ctsi/set-overlay-pos-type interaction-popup :top-left base-frame objects)
overlay-pos (ctsi/calc-overlay-position i2 rect objects popup base-frame overlay-frame frame-offset)]
[overlay-pos [snap-v snap-h]] (ctsi/calc-overlay-position i2 rect objects popup base-frame overlay-frame frame-offset)]
(t/is (= (:x overlay-pos) 15))
(t/is (= (:y overlay-pos) 15))))
(t/is (= (:y overlay-pos) 15))
(t/is (= snap-v :top))
(t/is (= snap-h :left))))
(t/testing "Overlay top-center relative to popup"
(let [i2 (ctsi/set-overlay-pos-type interaction-popup :top-center base-frame objects)
overlay-pos (ctsi/calc-overlay-position i2 rect objects popup base-frame overlay-frame frame-offset)]
[overlay-pos [snap-v snap-h]] (ctsi/calc-overlay-position i2 rect objects popup base-frame overlay-frame frame-offset)]
(t/is (= (:x overlay-pos) 25))
(t/is (= (:y overlay-pos) 15))))
(t/is (= (:y overlay-pos) 15))
(t/is (= snap-v :top))
(t/is (= snap-h :center))))
(t/testing "Overlay top-right relative to popup"
(let [i2 (ctsi/set-overlay-pos-type interaction-popup :top-right base-frame objects)
overlay-pos (ctsi/calc-overlay-position i2 rect objects popup base-frame overlay-frame frame-offset)]
[overlay-pos [snap-v snap-h]] (ctsi/calc-overlay-position i2 rect objects popup base-frame overlay-frame frame-offset)]
(t/is (= (:x overlay-pos) 35))
(t/is (= (:y overlay-pos) 15))))
(t/is (= (:y overlay-pos) 15))
(t/is (= snap-v :top))
(t/is (= snap-h :right))))
(t/testing "Overlay bottom-left relative to popup"
(let [i2 (ctsi/set-overlay-pos-type interaction-popup :bottom-left base-frame objects)
overlay-pos (ctsi/calc-overlay-position i2 rect objects popup base-frame overlay-frame frame-offset)]
[overlay-pos [snap-v snap-h]] (ctsi/calc-overlay-position i2 rect objects popup base-frame overlay-frame frame-offset)]
(t/is (= (:x overlay-pos) 15))
(t/is (= (:y overlay-pos) 45))))
(t/is (= (:y overlay-pos) 45))
(t/is (= snap-v :bottom))
(t/is (= snap-h :left))))
(t/testing "Overlay bottom-center relative to popup"
(let [i2 (ctsi/set-overlay-pos-type interaction-popup :bottom-center base-frame objects)
overlay-pos (ctsi/calc-overlay-position i2 rect objects popup base-frame overlay-frame frame-offset)]
[overlay-pos [snap-v snap-h]] (ctsi/calc-overlay-position i2 rect objects popup base-frame overlay-frame frame-offset)]
(t/is (= (:x overlay-pos) 25))
(t/is (= (:y overlay-pos) 45))))
(t/is (= (:y overlay-pos) 45))
(t/is (= snap-v :bottom))
(t/is (= snap-h :center))))
(t/testing "Overlay bottom-right relative to popup"
(let [i2 (ctsi/set-overlay-pos-type interaction-popup :bottom-right base-frame objects)
overlay-pos (ctsi/calc-overlay-position i2 rect objects popup base-frame overlay-frame frame-offset)]
[overlay-pos [snap-v snap-h]] (ctsi/calc-overlay-position i2 rect objects popup base-frame overlay-frame frame-offset)]
(t/is (= (:x overlay-pos) 35))
(t/is (= (:y overlay-pos) 45))))
(t/is (= (:y overlay-pos) 45))
(t/is (= snap-v :bottom))
(t/is (= snap-h :right))))
(t/testing "Overlay center relative to popup"
(let [i2 (ctsi/set-overlay-pos-type interaction-popup :center base-frame objects)
overlay-pos (ctsi/calc-overlay-position i2 rect objects popup base-frame overlay-frame frame-offset)]
[overlay-pos [snap-v snap-h]] (ctsi/calc-overlay-position i2 rect objects popup base-frame overlay-frame frame-offset)]
(t/is (= (:x overlay-pos) 25))
(t/is (= (:y overlay-pos) 30))))
(t/is (= (:y overlay-pos) 30))
(t/is (= snap-v :center))
(t/is (= snap-h :center))))
(t/testing "Overlay manual relative to popup"
(let [i2 (-> interaction-popup
(ctsi/set-overlay-pos-type :manual base-frame objects)
(ctsi/set-overlay-position (gpt/point 12 62)))
overlay-pos (ctsi/calc-overlay-position i2 rect objects popup base-frame overlay-frame frame-offset)]
[overlay-pos [snap-v snap-h]] (ctsi/calc-overlay-position i2 rect objects popup base-frame overlay-frame frame-offset)]
(t/is (= (:x overlay-pos) 27))
(t/is (= (:y overlay-pos) 77))))
(t/is (= (:y overlay-pos) 77))
(t/is (= snap-v :top))
(t/is (= snap-h :left))))
(t/testing "Overlay top-left relative to popup"
(let [i2 (ctsi/set-overlay-pos-type interaction-popup :top-left base-frame objects)
overlay-pos (ctsi/calc-overlay-position i2 rect objects popup base-frame overlay-frame frame-offset)]
[overlay-pos [snap-v snap-h]] (ctsi/calc-overlay-position i2 rect objects popup base-frame overlay-frame frame-offset)]
(t/is (= (:x overlay-pos) 15))
(t/is (= (:y overlay-pos) 15))))
(t/is (= (:y overlay-pos) 15))
(t/is (= snap-v :top))
(t/is (= snap-h :left))))
(t/testing "Overlay top-center relative to rect"
(let [i2 (ctsi/set-overlay-pos-type interaction-rect :top-center base-frame objects)
overlay-pos (ctsi/calc-overlay-position i2 rect objects rect base-frame overlay-frame frame-offset)]
[overlay-pos [snap-v snap-h]] (ctsi/calc-overlay-position i2 rect objects rect base-frame overlay-frame frame-offset)]
(t/is (= (:x overlay-pos) 25))
(t/is (= (:y overlay-pos) 15))))
(t/is (= (:y overlay-pos) 15))
(t/is (= snap-v :top))
(t/is (= snap-h :center))))
(t/testing "Overlay top-right relative to rect"
(let [i2 (ctsi/set-overlay-pos-type interaction-rect :top-right base-frame objects)
overlay-pos (ctsi/calc-overlay-position i2 rect objects rect base-frame overlay-frame frame-offset)]
[overlay-pos [snap-v snap-h]] (ctsi/calc-overlay-position i2 rect objects rect base-frame overlay-frame frame-offset)]
(t/is (= (:x overlay-pos) 35))
(t/is (= (:y overlay-pos) 15))))
(t/is (= (:y overlay-pos) 15))
(t/is (= snap-v :top))
(t/is (= snap-h :right))))
(t/testing "Overlay bottom-left relative to rect"
(let [i2 (ctsi/set-overlay-pos-type interaction-rect :bottom-left base-frame objects)
overlay-pos (ctsi/calc-overlay-position i2 rect objects rect base-frame overlay-frame frame-offset)]
[overlay-pos [snap-v snap-h]] (ctsi/calc-overlay-position i2 rect objects rect base-frame overlay-frame frame-offset)]
(t/is (= (:x overlay-pos) 15))
(t/is (= (:y overlay-pos) 45))))
(t/is (= (:y overlay-pos) 45))
(t/is (= snap-v :bottom))
(t/is (= snap-h :left))))
(t/testing "Overlay bottom-center relative to rect"
(let [i2 (ctsi/set-overlay-pos-type interaction-rect :bottom-center base-frame objects)
overlay-pos (ctsi/calc-overlay-position i2 rect objects rect base-frame overlay-frame frame-offset)]
[overlay-pos [snap-v snap-h]] (ctsi/calc-overlay-position i2 rect objects rect base-frame overlay-frame frame-offset)]
(t/is (= (:x overlay-pos) 25))
(t/is (= (:y overlay-pos) 45))))
(t/is (= (:y overlay-pos) 45))
(t/is (= snap-v :bottom))
(t/is (= snap-h :center))))
(t/testing "Overlay bottom-right relative to rect"
(let [i2 (ctsi/set-overlay-pos-type interaction-rect :bottom-right base-frame objects)
overlay-pos (ctsi/calc-overlay-position i2 rect objects rect base-frame overlay-frame frame-offset)]
[overlay-pos [snap-v snap-h]] (ctsi/calc-overlay-position i2 rect objects rect base-frame overlay-frame frame-offset)]
(t/is (= (:x overlay-pos) 35))
(t/is (= (:y overlay-pos) 45))))
(t/is (= (:y overlay-pos) 45))
(t/is (= snap-v :bottom))
(t/is (= snap-h :right))))
(t/testing "Overlay center relative to rect"
(let [i2 (ctsi/set-overlay-pos-type interaction-rect :center base-frame objects)
overlay-pos (ctsi/calc-overlay-position i2 rect objects rect base-frame overlay-frame frame-offset)]
[overlay-pos [snap-v snap-h]] (ctsi/calc-overlay-position i2 rect objects rect base-frame overlay-frame frame-offset)]
(t/is (= (:x overlay-pos) 25))
(t/is (= (:y overlay-pos) 30))))
(t/is (= (:y overlay-pos) 30))
(t/is (= snap-v :center))
(t/is (= snap-h :center))))
(t/testing "Overlay manual relative to rect"
(let [i2 (-> interaction-rect
(ctsi/set-overlay-pos-type :manual base-frame objects)
(ctsi/set-overlay-position (gpt/point 12 62)))
overlay-pos (ctsi/calc-overlay-position i2 rect objects rect base-frame overlay-frame frame-offset)]
[overlay-pos [snap-v snap-h]] (ctsi/calc-overlay-position i2 rect objects rect base-frame overlay-frame frame-offset)]
(t/is (= (:x overlay-pos) 17))
(t/is (= (:y overlay-pos) 67))))))
(t/is (= (:y overlay-pos) 67))
(t/is (= snap-v :top))
(t/is (= snap-h :left))))))
(t/deftest animation-checks

View file

@ -290,6 +290,7 @@
border-radius: $br4;
margin: 0.5rem;
cursor: pointer;
text-decoration: none;
&:hover {
background-color: $color-primary;

View file

@ -378,6 +378,8 @@ span.element-name {
background-color: $color-gray-50;
color: $color-white;
font-size: $fs12;
flex-grow: 1;
margin: 0;
height: 16px;
&:focus {
outline: none;
@ -386,10 +388,16 @@ span.element-name {
div {
height: 16px;
overflow: hidden;
width: 100%;
display: flex;
align-items: center;
}
.filter,
.clear {
width: 35px;
display: flex;
justify-content: center;
align-items: center;
&.active {
svg {
fill: $color-primary;

View file

@ -314,9 +314,18 @@
(ptk/reify ::retrieve-unread-comment-threads
ptk/WatchEvent
(watch [_ _ _]
(let [fetched #(assoc %2 :comment-threads (d/index-by :id %1))]
(let [fetched-comments #(assoc %2 :comment-threads (d/index-by :id %1))
fetched-users #(assoc %2 :current-team-comments-users %1)]
(->> (rp/cmd! :get-unread-comment-threads {:team-id team-id})
(rx/map #(partial fetched %))
(rx/merge-map
(fn [comments]
(rx/concat
(rx/of (partial fetched-comments comments))
(->> (rx/from (map :file-id comments))
(rx/merge-map #(rp/cmd! :get-profiles-for-file-comments {:file-id %}))
(rx/reduce #(merge %1 (d/index-by :id %2)) {})
(rx/map #(partial fetched-users %))))))
(rx/catch #(rx/throw {:type :comment-error})))))))

View file

@ -542,13 +542,14 @@
;; --- Overlays
(defn- open-overlay*
[state frame position close-click-outside background-overlay animation]
[state frame position snap-to close-click-outside background-overlay animation]
(cond-> state
:always
(update :viewer-overlays conj
{:frame frame
:id (:id frame)
:position position
:snap-to snap-to
:close-click-outside close-click-outside
:background-overlay background-overlay
:animation animation})
@ -571,7 +572,7 @@
:animation animation})))
(defn open-overlay
[frame-id position close-click-outside background-overlay animation]
[frame-id position snap-to close-click-outside background-overlay animation]
(dm/assert! (uuid? frame-id))
(dm/assert! (gpt/point? position))
(dm/assert! (or (nil? close-click-outside)
@ -593,6 +594,7 @@
(open-overlay* state
frame
position
snap-to
close-click-outside
background-overlay
animation)
@ -600,7 +602,7 @@
(defn toggle-overlay
[frame-id position close-click-outside background-overlay animation]
[frame-id position snap-to close-click-outside background-overlay animation]
(dm/assert! (uuid? frame-id))
(dm/assert! (gpt/point? position))
(dm/assert! (or (nil? close-click-outside)
@ -623,6 +625,7 @@
(open-overlay* state
frame
position
snap-to
close-click-outside
background-overlay
animation)

View file

@ -23,6 +23,7 @@
[app.common.text :as txt]
[app.common.transit :as t]
[app.common.types.component :as ctk]
[app.common.types.components-list :as ctkl]
[app.common.types.container :as ctn]
[app.common.types.file :as ctf]
[app.common.types.pages-list :as ctpl]
@ -450,18 +451,49 @@
(ptk/reify ::duplicate-page
ptk/WatchEvent
(watch [it state _]
(let [id (uuid/next)
pages (get-in state [:workspace-data :pages-index])
unames (cfh/get-used-names pages)
page (get-in state [:workspace-data :pages-index page-id])
name (cfh/generate-unique-name unames (:name page))
(let [id (uuid/next)
pages (get-in state [:workspace-data :pages-index])
unames (cfh/get-used-names pages)
page (get-in state [:workspace-data :pages-index page-id])
name (cfh/generate-unique-name unames (:name page))
fdata (:workspace-data state)
components-v2 (dm/get-in fdata [:options :components-v2])
objects (->> (:objects page)
(d/mapm (fn [_ val] (dissoc val :use-for-thumbnail?))))
main-instances-ids (set (keep #(when (ctk/main-instance? (val %)) (key %)) objects))
ids-to-remove (set (apply concat (map #(cph/get-children-ids objects %) main-instances-ids)))
add-component-copy
(fn [objs id shape]
(let [component (ctkl/get-component fdata (:component-id shape))
[new-shape new-shapes]
(ctn/make-component-instance page
component
fdata
(gpt/point (:x shape) (:y shape))
components-v2
{:keep-ids? true})
children (into {} (map (fn [shape] [(:id shape) shape]) new-shapes))
objs (assoc objs id new-shape)]
(merge objs children)))
objects
(reduce
(fn [objs [id shape]]
(cond (contains? main-instances-ids id)
(add-component-copy objs id shape)
(contains? ids-to-remove id)
objs
:else
(assoc objs id shape)))
{}
objects)
page (-> page
(assoc :name name)
(assoc :id id)
(assoc :objects
(->> (:objects page)
(d/mapm (fn [_ val] (dissoc val :use-for-thumbnail?))))))
objects))
changes (-> (pcb/empty-changes it)
(pcb/add-page id page))]
@ -719,17 +751,18 @@
groups-to-delete groups-to-unmask shapes-to-detach
shapes-to-reroot shapes-to-deroot shapes-to-unconstraint]
(let [ordered-indexes (cph/order-by-indexed-shapes objects ids)
shapes (map (d/getf objects) ordered-indexes)]
shapes (map (d/getf objects) ordered-indexes)
parent (get objects parent-id)]
(-> (pcb/empty-changes it page-id)
(pcb/with-objects objects)
;; Remove layout-item properties when moving a shape outside a layout
(cond-> (not (ctl/any-layout? objects parent-id))
(cond-> (not (ctl/any-layout? parent))
(pcb/update-shapes ordered-indexes ctl/remove-layout-item-data))
;; Remove the hide in viewer flag
(cond-> (and (not= uuid/zero parent-id) (cph/frame-shape? objects parent-id))
(cond-> (and (not= uuid/zero parent-id) (cph/frame-shape? parent))
(pcb/update-shapes ordered-indexes #(cond-> % (cph/frame-shape? %) (assoc :hide-in-viewer true))))
;; Move the shapes
@ -761,8 +794,7 @@
;; Reset constraints depending on the new parent
(pcb/update-shapes shapes-to-unconstraint
(fn [shape]
(let [parent (get objects parent-id)
frame-id (if (= (:type parent) :frame)
(let [frame-id (if (= (:type parent) :frame)
(:id parent)
(:frame-id parent))
moved-shape (assoc shape
@ -797,6 +829,10 @@
(pcb/reorder-grid-children parents)
;; If parent locked, lock the added shapes
(cond-> (:blocked parent)
(pcb/update-shapes ordered-indexes #(assoc % :blocked true)))
;; Resize parent containers that need to
(pcb/resize-parents parents))))
@ -1706,7 +1742,8 @@
[(:frame-id base) parent-id delta index])
;; Paste inside selected frame otherwise
(let [origin-frame-id (:frame-id first-selected-obj)
(let [selected-frame-obj (get page-objects (first page-selected))
origin-frame-id (:frame-id first-selected-obj)
origin-frame-object (get page-objects origin-frame-id)
margin-x (-> (- (:width origin-frame-object) (+ (:x wrapper) (:width wrapper)))
@ -1735,7 +1772,7 @@
;; - Align it to the limits on the x and y axis
;; - Respect the distance of the object to the right and bottom in the original frame
(gpt/point paste-x paste-y))]
[frame-id frame-id delta]))
[frame-id frame-id delta (dec (count (:shapes selected-frame-obj )))]))
(empty? page-selected)
(let [frame-id (ctst/top-nested-frame page-objects mouse-pos)

View file

@ -181,7 +181,9 @@
(on-error error)
:else
(rx/throw error))))]
(do
(.error js/console "ERROR" error)
(rx/of (msg/error (tr "errors.cannot-upload")))))))]
(ptk/reify ::process-media-objects
ptk/WatchEvent

View file

@ -407,6 +407,32 @@
(assoc state :workspace-modifiers modif-tree))))))
;; This function is similar to set-rotation-modifiers but:
;; - It consideres the center for everyshape instead of the center of the total selrect
;; - The angle param is the desired final value, not a delta
(defn set-delta-rotation-modifiers
([angle shapes]
(ptk/reify ::set-delta-rotation-modifiers
ptk/UpdateEvent
(update [_ state]
(let [objects (wsh/lookup-page-objects state)
ids
(->> shapes
(remove #(get % :blocked false))
(filter #(contains? (get editable-attrs (:type %)) :rotation))
(map :id))
get-modifier
(fn [shape]
(let [delta (- angle (:rotation shape))
center (gsh/shape->center shape)]
(ctm/rotation-modifiers shape center delta)))
modif-tree
(-> (build-modif-tree ids objects get-modifier)
(gsh/set-objects-modifiers objects))]
(assoc state :workspace-modifiers modif-tree))))))
(defn apply-modifiers
([]

View file

@ -344,12 +344,10 @@
(let [page-id (:current-page-id state)
objects (wsh/lookup-page-objects state page-id)
rotate-shape (fn [shape]
(let [delta (- rotation (:rotation shape))]
(dwm/set-rotation-modifiers delta [shape])))]
shapes (->> ids (map #(get objects %)))]
(rx/concat
(rx/from (->> ids (map #(get objects %)) (map rotate-shape)))
(rx/of (dwm/apply-modifiers)))))))
(rx/of (dwm/set-delta-rotation-modifiers rotation shapes))
(rx/of (dwm/apply-modifiers)))))))
;; -- Move ----------------------------------------------------------

View file

@ -459,6 +459,9 @@
(def current-file-comments-users
(l/derived :current-file-comments-users st/state))
(def current-team-comments-users
(l/derived :current-team-comments-users st/state))
(def viewer-fullscreen?
(l/derived (fn [state]
(dm/get-in state [:viewer-local :fullscreen?]))

View file

@ -32,7 +32,7 @@
show-dropdown (mf/use-fn #(reset! show-dropdown? true))
hide-dropdown (mf/use-fn #(reset! show-dropdown? false))
threads-map (mf/deref refs/comment-threads)
users (mf/deref refs/current-file-comments-users)
users (mf/deref refs/current-team-comments-users)
tgroups (->> (vals threads-map)
(sort-by :modified-at)

View file

@ -18,6 +18,7 @@
[app.util.dom :as dom]
[app.util.i18n :as i18n :refer [tr]]
[app.util.keyboard :as kbd]
[app.util.router :as rt]
[app.util.webapi :as wapi]
[beicon.core :as rx]
[cuerdas.core :as str]
@ -142,12 +143,22 @@
(sort-by :modified-at)
(reverse)))
on-file-created
(mf/use-fn
(fn [data]
(let [pparams {:project-id (:project-id data)
:file-id (:id data)}
qparams {:page-id (get-in data [:data :pages 0])}]
(st/emit! (rt/nav :workspace pparams qparams)))))
create-file
(mf/use-fn
(mf/deps project)
(fn [origin]
(st/emit! (with-meta (dd/create-file {:project-id (:id project)})
{::ev/origin origin}))))]
(let [mdata {:on-success on-file-created}
params {:project-id (:id project)}]
(st/emit! (-> (dd/create-file (with-meta params mdata))
(with-meta {::ev/origin origin}))))))]
(mf/with-effect []
(let [node (mf/ref-val rowref)

View file

@ -31,23 +31,27 @@
[rumext.v2 :as mf]))
(mf/defc header
{::mf/wrap [mf/memo]}
[{:keys [section team] :as props}]
(let [go-members (mf/use-fn #(st/emit! (dd/go-to-team-members)))
go-settings (mf/use-fn #(st/emit! (dd/go-to-team-settings)))
go-invitations (mf/use-fn #(st/emit! (dd/go-to-team-invitations)))
go-webhooks (mf/use-fn #(st/emit! (dd/go-to-team-webhooks)))
invite-member (mf/use-fn
(mf/deps team)
#(st/emit! (modal/show {:type :invite-members
:team team
:origin :team})))
{::mf/wrap [mf/memo]
::mf/wrap-props false}
[{:keys [section team]}]
(let [on-nav-members (mf/use-fn #(st/emit! (dd/go-to-team-members)))
on-nav-settings (mf/use-fn #(st/emit! (dd/go-to-team-settings)))
on-nav-invitations (mf/use-fn #(st/emit! (dd/go-to-team-invitations)))
on-nav-webhooks (mf/use-fn #(st/emit! (dd/go-to-team-webhooks)))
members-section? (= section :dashboard-team-members)
settings-section? (= section :dashboard-team-settings)
invitations-section? (= section :dashboard-team-invitations)
webhooks-section? (= section :dashboard-team-webhooks)
permissions (:permissions team)]
permissions (:permissions team)
on-invite-member
(mf/use-fn
(mf/deps team)
(fn []
(st/emit! (modal/show {:type :invite-members
:team team
:origin :team}))))]
[:header.dashboard-header.team
[:div.dashboard-title
@ -60,17 +64,19 @@
[:nav.dashboard-header-menu
[:ul.dashboard-header-options
[:li {:class (when members-section? "active")}
[:a {:on-click go-members} (tr "labels.members")]]
[:a {:on-click on-nav-members} (tr "labels.members")]]
[:li {:class (when invitations-section? "active")}
[:a {:on-click go-invitations} (tr "labels.invitations")]]
[:a {:on-click on-nav-invitations} (tr "labels.invitations")]]
(when (contains? cfg/flags :webhooks)
[:li {:class (when webhooks-section? "active")}
[:a {:on-click go-webhooks} (tr "labels.webhooks")]])
[:a {:on-click on-nav-webhooks} (tr "labels.webhooks")]])
[:li {:class (when settings-section? "active")}
[:a {:on-click go-settings} (tr "labels.settings")]]]]
[:a {:on-click on-nav-settings} (tr "labels.settings")]]]]
[:div.dashboard-buttons
(if (and (or invitations-section? members-section?) (:is-admin permissions))
[:a.btn-secondary.btn-small {:on-click invite-member :data-test "invite-member"}
[:a.btn-secondary.btn-small
{:on-click on-invite-member
:data-test "invite-member"}
(tr "dashboard.invite-profile")]
[:div.blank-space])]]))
@ -98,27 +104,29 @@
(mf/defc invite-members-modal
{::mf/register modal/components
::mf/register-as :invite-members}
::mf/register-as :invite-members
::mf/wrap-props false}
[{:keys [team origin]}]
(let [members-map (mf/deref refs/dashboard-team-members)
perms (:permissions team)
perms (:permissions team)
roles (mf/use-memo (mf/deps perms) #(get-available-roles perms))
initial (mf/use-memo (constantly {:role "editor" :team-id (:id team)}))
form (fm/use-form :spec ::invite-member-form
:initial initial)
error-text (mf/use-state "")
on-success
(fn []
(st/emit! (msg/success (tr "notifications.invitation-email-sent"))
(modal/hide)
(dd/fetch-team-invitations)))
roles (mf/use-memo (mf/deps perms) #(get-available-roles perms))
initial (mf/use-memo (constantly {:role "editor" :team-id (:id team)}))
form (fm/use-form :spec ::invite-member-form
:initial initial)
error-text (mf/use-state "")
current-data-emails (into #{} (dm/get-in @form [:clean-data :emails]))
current-members-emails (into #{} (map (comp :email second)) members-map)
on-success
(fn [_form {:keys [total]}]
(when (pos? total)
(st/emit! (msg/success (tr "notifications.invitation-email-sent"))))
(st/emit! (modal/hide)
(dd/fetch-team-invitations)))
on-error
(fn [{:keys [type code] :as error}]
(cond
@ -185,7 +193,9 @@
;; MEMBERS SECTION
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(mf/defc member-info [{:keys [member profile] :as props}]
(mf/defc member-info
{::mf/wrap-props false}
[{:keys [member profile]}]
(let [is-you? (= (:id profile) (:id member))]
[:*
[:div.member-image
@ -196,93 +206,97 @@
[:span.you (tr "labels.you")])]
[:div.member-email (:email member)]]]))
(mf/defc rol-info [{:keys [member team set-admin set-editor set-owner profile] :as props}]
(mf/defc rol-info
{::mf/wrap-props false}
[{:keys [member team on-set-admin on-set-editor on-set-owner profile]}]
(let [member-is-owner? (:is-owner member)
member-is-admin? (and (:is-admin member) (not member-is-owner?))
member-is-editor? (and (:can-edit member) (and (not member-is-admin?) (not member-is-owner?)))
show? (mf/use-state false)
you-owner? (get-in team [:permissions :is-owner])
you-admin? (get-in team [:permissions :is-admin])
you-owner? (dm/get-in team [:permissions :is-owner])
you-admin? (dm/get-in team [:permissions :is-admin])
is-you? (= (:id profile) (:id member))
can-change-rol? (or you-owner? you-admin?)
not-superior? (or you-owner? (and can-change-rol? (or member-is-admin? member-is-editor?)))
role (cond
member-is-owner? "labels.owner"
member-is-admin? "labels.admin"
member-is-owner? "labels.owner"
member-is-admin? "labels.admin"
member-is-editor? "labels.editor"
:else "labels.viewer")
is-you? (= (:id profile) (:id member))]
:else "labels.viewer")
on-show (mf/use-fn #(reset! show? true))
on-hide (mf/use-fn #(reset! show? false))]
[:*
(if (and can-change-rol? not-superior? (not (and is-you? you-owner?)))
[:div.rol-selector.has-priv {:on-click #(reset! show? true)}
[:div.rol-selector.has-priv {:on-click on-show}
[:span.rol-label (tr role)]
[:span.icon i/arrow-down]]
[:div.rol-selector
[:span.rol-label (tr role)]])
[:& dropdown {:show @show?
:on-close #(reset! show? false)}
[:& dropdown {:show @show? :on-close on-hide}
[:ul.dropdown.options-dropdown
[:li {:on-click set-admin} (tr "labels.admin")]
[:li {:on-click set-editor} (tr "labels.editor")]
[:li {:on-click on-set-admin} (tr "labels.admin")]
[:li {:on-click on-set-editor} (tr "labels.editor")]
;; Temporarily disabled viewer role
;; https://tree.taiga.io/project/penpot/issue/1083
;; [:li {:on-click set-viewer} (tr "labels.viewer")]
(when you-owner?
[:li {:on-click (partial set-owner member)} (tr "labels.owner")])]]]))
[:li {:on-click (partial on-set-owner member)} (tr "labels.owner")])]]]))
(mf/defc member-actions
{::mf/wrap-props false}
[{:keys [member team on-delete on-leave profile]}]
(let [is-owner? (:is-owner member)
owner? (dm/get-in team [:permissions :is-owner])
admin? (dm/get-in team [:permissions :is-admin])
show? (mf/use-state false)
is-you? (= (:id profile) (:id member))
can-delete? (or owner? admin?)
on-show (mf/use-fn #(reset! show? true))
on-hide (mf/use-fn #(reset! show? false))]
(mf/defc member-actions [{:keys [member team delete leave profile] :as props}]
(let [is-owner? (:is-owner member)
owner? (get-in team [:permissions :is-owner])
admin? (get-in team [:permissions :is-admin])
show? (mf/use-state false)
is-you? (= (:id profile) (:id member))
can-delete? (or owner? admin?)]
[:*
(when (or is-you? (and can-delete? (not (and is-owner? (not owner?)))))
[:span.icon {:on-click #(reset! show? true)} [i/actions]])
[:& dropdown {:show @show?
:on-close #(reset! show? false)}
[:span.icon {:on-click on-show} [i/actions]])
[:& dropdown {:show @show? :on-close on-hide}
[:ul.dropdown.actions-dropdown
(when is-you?
[:li {:on-click leave} (tr "dashboard.leave-team")])
[:li {:on-click on-leave} (tr "dashboard.leave-team")])
(when (and can-delete? (not is-you?) (not (and is-owner? (not owner?))))
[:li {:on-click delete} (tr "labels.remove-member")])]]]))
[:li {:on-click on-delete} (tr "labels.remove-member")])]]]))
(defn- set-role! [member-id role]
(let [params {:member-id member-id :role role}]
(st/emit! (dd/update-team-member-role params))))
(mf/defc team-member
{::mf/wrap [mf/memo]}
[{:keys [team member members profile] :as props}]
{::mf/wrap [mf/memo]
::mf/wrap-props false}
[{:keys [team member members profile]}]
(let [owner? (dm/get-in team [:permissions :is-owner])
set-role
(let [member-id (:id member)
on-set-admin (mf/use-fn (mf/deps member-id) (partial set-role! member-id :admin))
on-set-editor (mf/use-fn (mf/deps member-id) (partial set-role! member-id :editor))
owner? (dm/get-in team [:permissions :is-owner])
on-set-owner
(mf/use-fn
(mf/deps member)
(fn [role]
(let [params {:member-id (:id member) :role role}]
(st/emit! (dd/update-team-member-role params)))))
set-owner-fn (mf/use-fn (mf/deps set-role) (partial set-role :owner))
set-admin (mf/use-fn (mf/deps set-role) (partial set-role :admin))
set-editor (mf/use-fn (mf/deps set-role) (partial set-role :editor))
;; set-viewer (partial set-role :viewer)
set-owner
(mf/use-fn
(mf/deps set-owner-fn member)
(fn [member]
(st/emit! (modal/show
{:type :confirm
:title (tr "modals.promote-owner-confirm.title")
:message (tr "modals.promote-owner-confirm.message" (:name member))
:scd-message (tr "modals.promote-owner-confirm.hint")
:accept-label (tr "modals.promote-owner-confirm.accept")
:on-accept set-owner-fn
:accept-style :primary}))))
delete-member-fn
(mf/use-fn
(mf/deps member)
(fn [] (st/emit! (dd/delete-team-member {:member-id (:id member)}))))
(fn [member _event]
(let [params {:type :confirm
:title (tr "modals.promote-owner-confirm.title")
:message (tr "modals.promote-owner-confirm.message" (:name member))
:scd-message (tr "modals.promote-owner-confirm.hint")
:accept-label (tr "modals.promote-owner-confirm.accept")
:on-accept (partial set-role! member-id :owner)
:accept-style :primary}]
(st/emit! (modal/show params)))))
on-success
(mf/use-fn
@ -308,14 +322,14 @@
(rx/throw error))))
delete-fn
on-delete-accepted
(mf/use-fn
(mf/deps team on-success on-error)
(fn []
(st/emit! (dd/delete-team (with-meta team {:on-success on-success
:on-error on-error})))))
leave-fn
on-leave-accepted
(mf/use-fn
(mf/deps on-success on-error)
(fn [member-id]
@ -324,9 +338,9 @@
{:on-success on-success
:on-error on-error}))))))
leave-and-close
on-leave-and-close
(mf/use-fn
(mf/deps delete-fn)
(mf/deps on-delete-accepted)
(fn []
(st/emit! (modal/show
{:type :confirm
@ -334,80 +348,100 @@
:message (tr "modals.leave-and-close-confirm.message" (:name team))
:scd-message (tr "modals.leave-and-close-confirm.hint")
:accept-label (tr "modals.leave-confirm.accept")
:on-accept delete-fn}))))
:on-accept on-delete-accepted}))))
change-owner-and-leave
on-change-owner-and-leave
(mf/use-fn
(mf/deps profile team leave-fn)
(mf/deps profile team on-leave-accepted)
(fn []
(st/emit! (dd/fetch-team-members)
(modal/show
{:type :leave-and-reassign
:profile profile
:team team
:accept leave-fn}))))
:accept on-leave-accepted}))))
leave
on-leave
(mf/use-fn
(mf/deps leave-fn)
(mf/deps on-leave-accepted)
(fn []
(st/emit! (modal/show
{:type :confirm
:title (tr "modals.leave-confirm.title")
:message (tr "modals.leave-confirm.message")
:accept-label (tr "modals.leave-confirm.accept")
:on-accept leave-fn}))))
:on-accept on-leave-accepted}))))
preset-leave (cond (= 1 (count members)) leave-and-close
(= true owner?) change-owner-and-leave
:else leave)
delete
on-delete
(mf/use-fn
(mf/deps delete-member-fn)
(mf/deps member-id)
(fn []
(st/emit! (modal/show
{:type :confirm
:title (tr "modals.delete-team-member-confirm.title")
:message (tr "modals.delete-team-member-confirm.message")
:accept-label (tr "modals.delete-team-member-confirm.accept")
:on-accept delete-member-fn}))))]
(let [on-accept #(st/emit! (dd/delete-team-member {:member-id member-id}))
params {:type :confirm
:title (tr "modals.delete-team-member-confirm.title")
:message (tr "modals.delete-team-member-confirm.message")
:accept-label (tr "modals.delete-team-member-confirm.accept")
:on-accept on-accept}]
(st/emit! (modal/show params)))))
on-leave'
(cond (= 1 (count members)) on-leave-and-close
(= true owner?) on-change-owner-and-leave
:else on-leave)]
[:div.table-row
[:div.table-field.name
[:& member-info {:member member :profile profile}]]
[:div.table-field.roles
[:& rol-info {:member member
:team team
:set-admin set-admin
:set-editor set-editor
:set-owner set-owner
:on-set-admin on-set-admin
:on-set-editor on-set-editor
:on-set-owner on-set-owner
:profile profile}]]
[:div.table-field.actions
[:& member-actions {:member member
:profile profile
:team team
:delete delete
:leave preset-leave}]]]))
:on-delete on-delete
:on-leave on-leave'}]]]))
(mf/defc team-members
[{:keys [members-map team profile] :as props}]
(let [members (->> (vals members-map)
(sort-by :created-at)
(remove :is-owner))
owner (->> (vals members-map)
(d/seek :is-owner))]
{::mf/wrap-props false}
[{:keys [members-map team profile]}]
(let [members (mf/with-memo [members-map]
(->> (vals members-map)
(sort-by :created-at)
(remove :is-owner)))
owner (mf/with-memo [members-map]
(->> (vals members-map)
(d/seek :is-owner)))]
[:div.dashboard-table.team-members
[:div.table-header
[:div.table-field.name (tr "labels.member")]
[:div.table-field.role (tr "labels.role")]]
[:div.table-rows
[:& team-member {:member owner :team team :profile profile :members members-map}]
[:& team-member
{:member owner
:team team
:profile profile
:members members-map}]
(for [item members]
[:& team-member {:member item :team team :profile profile :key (:id item) :members members-map}])]]))
[:& team-member
{:member item
:team team
:profile profile
:key (:id item)
:members members-map}])]]))
(mf/defc team-members-page
[{:keys [team profile] :as props}]
{::mf/wrap-props false}
[{:keys [team profile]}]
(let [members-map (mf/deref refs/dashboard-team-members)]
(mf/with-effect [team]
@ -417,74 +451,76 @@
(tr "dashboard.your-penpot")
(:name team)))))
(mf/with-effect
(mf/with-effect []
(st/emit! (dd/fetch-team-members)))
[:*
[:& header {:section :dashboard-team-members
:team team}]
[:& header {:section :dashboard-team-members :team team}]
[:section.dashboard-container.dashboard-team-members
[:& team-members {:profile profile
:team team
:members-map members-map}]]]))
[:& team-members
{:profile profile
:team team
:members-map members-map}]]]))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; INVITATIONS SECTION
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(mf/defc invitation-role-selector
[{:keys [can-invite? role status change-to-admin change-to-editor] :as props}]
(let [show? (mf/use-state false)
role-label (cond
(= role :owner) "labels.owner"
(= role :admin) "labels.admin"
(= role :editor) "labels.editor"
:else "labels.viewer")]
{::mf/wrap-props false}
[{:keys [can-invite? role status on-change]}]
(let [show? (mf/use-state false)
label (cond
(= role :owner) (tr "labels.owner")
(= role :admin) (tr "labels.admin")
(= role :editor) (tr "labels.editor")
:else (tr "labels.viewer"))
on-hide (mf/use-fn #(reset! show? false))
on-show (mf/use-fn #(reset! show? true))
on-change'
(mf/use-fn
(mf/deps on-change)
(fn [event]
(let [role (-> (dom/get-current-target event)
(dom/get-data "role")
(keyword))]
(on-change role event))))]
[:*
(if (and can-invite? (= status :pending))
[:div.rol-selector.has-priv {:on-click #(reset! show? true)}
[:span.rol-label (tr role-label)]
[:div.rol-selector.has-priv {:on-click on-show}
[:span.rol-label label]
[:span.icon i/arrow-down]]
[:div.rol-selector
[:span.rol-label (tr role-label)]])
[:span.rol-label label]])
[:& dropdown {:show @show?
:on-close #(reset! show? false)}
[:& dropdown {:show @show? :on-close on-hide}
[:ul.dropdown.options-dropdown
[:li {:on-click change-to-admin} (tr "labels.admin")]
[:li {:on-click change-to-editor} (tr "labels.editor")]]]]))
[:li {:data-role "admin" :on-click on-change'} (tr "labels.admin")]
[:li {:data-role "editor" :on-click on-change'} (tr "labels.editor")]]]]))
(mf/defc invitation-status-badge
[{:keys [status] :as props}]
(let [status-label (if (= status :expired)
(tr "labels.expired-invitation")
(tr "labels.pending-invitation"))]
[:div.status-badge {:class (dom/classnames
:expired (= status :expired)
:pending (= status :pending))}
[:span.status-label (tr status-label)]]))
{::mf/wrap-props false}
[{:keys [status]}]
[:div.status-badge
{:class (dom/classnames
:expired (= status :expired)
:pending (= status :pending))}
[:span.status-label
(if (= status :expired)
(tr "labels.expired-invitation")
(tr "labels.pending-invitation"))]])
(mf/defc invitation-actions
[{:keys [invitation team] :as props}]
{::mf/wrap-props false}
[{:keys [invitation team-id]}]
(let [show? (mf/use-state false)
team-id (:id team)
email (:email invitation)
role (:role invitation)
on-resend-success
(mf/use-fn
(fn []
(st/emit! (msg/success (tr "notifications.invitation-email-sent"))
(modal/hide)
(dd/fetch-team-invitations))))
on-copy-success
(mf/use-fn
(fn []
(st/emit! (msg/success (tr "notifications.invitation-link-copied"))
(modal/hide))))
on-error
(mf/use-fn
(mf/deps email)
@ -505,7 +541,7 @@
:else
(rx/throw error))))
delete-fn
on-delete
(mf/use-fn
(mf/deps email team-id)
(fn []
@ -513,7 +549,15 @@
mdata {:on-success #(st/emit! (dd/fetch-team-invitations))}]
(st/emit! (dd/delete-team-invitation (with-meta params mdata))))))
resend-fn
on-resend-success
(mf/use-fn
(fn []
(st/emit! (msg/success (tr "notifications.invitation-email-sent"))
(modal/hide)
(dd/fetch-team-invitations))))
on-resend
(mf/use-fn
(mf/deps email team-id)
(fn []
@ -527,7 +571,13 @@
(-> (dd/invite-team-members params)
(with-meta {::ev/origin :team}))))))
copy-fn
on-copy-success
(mf/use-fn
(fn []
(st/emit! (msg/success (tr "notifications.invitation-link-copied"))
(modal/hide))))
on-copy
(mf/use-fn
(mf/deps email team-id)
(fn []
@ -536,52 +586,55 @@
:on-error on-error})]
(st/emit!
(-> (dd/copy-invitation-link params)
(with-meta {::ev/origin :team}))))))]
(with-meta {::ev/origin :team}))))))
on-hide (mf/use-fn #(reset! show? false))
on-show (mf/use-fn #(reset! show? true))]
[:*
[:span.icon {:on-click #(reset! show? true)} [i/actions]]
[:& dropdown {:show @show?
:on-close #(reset! show? false)}
[:span.icon {:on-click on-show} [i/actions]]
[:& dropdown {:show @show? :on-close on-hide}
[:ul.dropdown.actions-dropdown
[:li {:on-click copy-fn} (tr "labels.copy-invitation-link")]
[:li {:on-click resend-fn} (tr "labels.resend-invitation")]
[:li {:on-click delete-fn} (tr "labels.delete-invitation")]]]]))
[:li {:on-click on-copy} (tr "labels.copy-invitation-link")]
[:li {:on-click on-resend} (tr "labels.resend-invitation")]
[:li {:on-click on-delete} (tr "labels.delete-invitation")]]]]))
(mf/defc invitation-row
{::mf/wrap [mf/memo]}
[{:keys [invitation can-invite? team] :as props}]
{::mf/wrap [mf/memo]
::mf/wrap-props false}
[{:keys [invitation can-invite? team-id] :as props}]
(let [expired? (:expired invitation)
email (:email invitation)
role (:role invitation)
status (if expired? :expired :pending)
change-rol
on-change-role
(mf/use-fn
(mf/deps team email)
(fn [role]
(let [params {:email email :team-id (:id team) :role role}
(mf/deps email team-id)
(fn [role _event]
(let [params {:email email :team-id team-id :role role}
mdata {:on-success #(st/emit! (dd/fetch-team-invitations))}]
(st/emit! (dd/update-team-invitation-role (with-meta params mdata))))))]
[:div.table-row
[:div.table-field.mail email]
[:div.table-field.roles
[:& invitation-role-selector
{:can-invite? can-invite?
:role role
:status status
:change-to-editor (partial change-rol :editor)
:change-to-admin (partial change-rol :admin)}]]
:on-change on-change-role}]]
[:div.table-field.status
[:& invitation-status-badge {:status status}]]
[:div.table-field.actions
(when can-invite?
[:& invitation-actions
{:invitation invitation
:team team}])]]))
:team-id team-id}])]]))
(mf/defc empty-invitation-table
[{:keys [can-invite?] :as props}]
@ -595,7 +648,8 @@
[{:keys [team invitations] :as props}]
(let [owner? (dm/get-in team [:permissions :is-owner])
admin? (dm/get-in team [:permissions :is-admin])
can-invite? (or owner? admin?)]
can-invite? (or owner? admin?)
team-id (:id team)]
[:div.dashboard-table.invitations
[:div.table-header
@ -610,7 +664,7 @@
{:key (:email invitation)
:invitation invitation
:can-invite? can-invite?
:team team}])])]))
:team-id team-id}])])]))
(mf/defc team-invitations-page
[{:keys [team] :as props}]
@ -767,6 +821,7 @@
(mf/defc webhooks-hero
{::mf/wrap-props false}
[]
[:div.banner
[:div.title (tr "labels.webhooks")
@ -785,18 +840,22 @@
[:span (tr "dashboard.webhooks.create")]]]])
(mf/defc webhook-actions
[{:keys [on-edit on-delete] :as props}]
(let [show? (mf/use-state false)]
{::mf/wrap-props false}
[{:keys [on-edit on-delete]}]
(let [show? (mf/use-state false)
on-show (mf/use-fn #(reset! show? true))
on-hide (mf/use-fn #(reset! show? false))]
[:*
[:span.icon {:on-click #(reset! show? true)} [i/actions]]
[:& dropdown {:show @show?
:on-close #(reset! show? false)}
[:span.icon {:on-click on-show} [i/actions]]
[:& dropdown {:show @show? :on-close on-hide}
[:ul.dropdown.actions-dropdown
[:li {:on-click on-edit} (tr "labels.edit")]
[:li {:on-click on-delete} (tr "labels.delete")]]]]))
(mf/defc last-delivery-icon
[{:keys [success? text] :as props}]
{::mf/wrap-props false}
[{:keys [success? text]}]
[:div.last-delivery-icon
[:div.tooltip
[:div.label text]
@ -808,34 +867,44 @@
(mf/defc webhook-item
{::mf/wrap [mf/memo]}
[{:keys [webhook] :as props}]
(let [on-edit #(st/emit! (modal/show :webhook {:webhook webhook}))
error-code (:error-code webhook)
(let [error-code (:error-code webhook)
id (:id webhook)
delete-fn
(fn []
(let [params {:id (:id webhook)}
mdata {:on-success #(st/emit! (dd/fetch-team-webhooks))}]
(st/emit! (dd/delete-team-webhook (with-meta params mdata)))))
on-edit
(mf/use-fn
(mf/deps webhook)
(fn []
(st/emit! (modal/show :webhook {:webhook webhook}))))
on-delete-accepted
(mf/use-fn
(mf/deps id)
(fn []
(let [params {:id id}
mdata {:on-success #(st/emit! (dd/fetch-team-webhooks))}]
(st/emit! (dd/delete-team-webhook (with-meta params mdata))))))
on-delete
(fn []
(st/emit! (modal/show
{:type :confirm
:title (tr "modals.delete-webhook.title")
:message (tr "modals.delete-webhook.message")
:accept-label (tr "modals.delete-webhook.accept")
:on-accept delete-fn})))
(mf/use-fn
(mf/deps on-delete-accepted)
(fn []
(let [params {:type :confirm
:title (tr "modals.delete-webhook.title")
:message (tr "modals.delete-webhook.message")
:accept-label (tr "modals.delete-webhook.accept")
:on-accept on-delete-accepted}]
(st/emit! (modal/show params)))))
last-delivery-text
(if (nil? error-code)
(tr "webhooks.last-delivery.success")
(str (tr "errors.webhooks.last-delivery")
(cond
(= error-code "ssl-validation-error")
(dm/str " " (tr "errors.webhooks.ssl-validation"))
(dm/str (tr "errors.webhooks.last-delivery")
(cond
(= error-code "ssl-validation-error")
(dm/str " " (tr "errors.webhooks.ssl-validation"))
(str/starts-with? error-code "unexpected-status")
(dm/str " " (tr "errors.webhooks.unexpected-status" (extract-status error-code))))))]
(str/starts-with? error-code "unexpected-status")
(dm/str " " (tr "errors.webhooks.unexpected-status" (extract-status error-code))))))]
[:div.table-row
[:div.table-field.last-delivery
@ -855,14 +924,16 @@
:on-delete on-delete}]]]))
(mf/defc webhooks-list
[{:keys [webhooks] :as props}]
{::mf/wrap-props false}
[{:keys [webhooks]}]
[:div.dashboard-table
[:div.table-rows
(for [webhook webhooks]
[:& webhook-item {:webhook webhook :key (:id webhook)}])]])
(mf/defc team-webhooks-page
[{:keys [team] :as props}]
{::mf/wrap-props false}
[{:keys [team]}]
(let [webhooks (mf/deref refs/dashboard-team-webhooks)]
(mf/with-effect [team]
@ -891,7 +962,8 @@
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(mf/defc team-settings-page
[{:keys [team] :as props}]
{::mf/wrap-props false}
[{:keys [team]}]
(let [finput (mf/use-ref)
members-map (mf/deref refs/dashboard-team-members)
@ -912,22 +984,19 @@
(st/emit! (dd/update-team-photo file)))]
(mf/use-effect
(mf/deps team)
(fn []
(dom/set-html-title (tr "title.team-settings"
(if (:is-default team)
(tr "dashboard.your-penpot")
(:name team))))))
(mf/with-effect [team]
(dom/set-html-title (tr "title.team-settings"
(if (:is-default team)
(tr "dashboard.your-penpot")
(:name team)))))
(mf/use-effect
#(st/emit! (dd/fetch-team-members)
(mf/with-effect []
(st/emit! (dd/fetch-team-members)
(dd/fetch-team-stats)))
[:*
[:& header {:section :dashboard-team-settings
:team team}]
[:& header {:section :dashboard-team-settings :team team}]
[:section.dashboard-container.dashboard-team-settings
[:div.team-settings
[:div.horizontal-blocks

View file

@ -146,8 +146,9 @@
is-component? (mf/use-ctx muc/is-component?)]
[:> frame-container props
[:g.frame-children {:opacity (:opacity shape)}
(for [item childs]
[:& shape-wrapper {:key (dm/str (:id item)) :shape item}])]
(for [{:keys [id] :as item} childs]
(when (some? id)
[:& shape-wrapper {:key (dm/str (:id item)) :shape item}]))]
(when (and is-component? (empty? childs))
[:& grid-layout-viewer {:shape shape :childs childs}])])))

View file

@ -50,6 +50,8 @@
(l/derived :viewer-overlays st/state))
(defn- calculate-size
"Calculate the total size we must reserve for the frame, including possible paddings
added because shadows or blur."
[objects frame zoom]
(let [{:keys [x y width height]} (gsb/get-object-bounds objects frame)]
{:base-width width
@ -60,6 +62,23 @@
:height (* height zoom)
:vbox (dm/fmt "% % % %" 0 0 width height)}))
(defn calculate-delta
"Calculate the displacement we need to apply so that the original selrect appears in the
same position as if it had no extra paddings, depending on the side the frame will
be snapped to."
[size selrect [snap-v snap-h] zoom]
(let [delta-x (case snap-h
:left (- (:x1 selrect) (:x size))
:right (- (:x2 selrect) (+ (:x size) (/ (:width size) zoom)))
:center (- (/ (- (:width selrect) (/ (:width size) zoom)) 2)
(- (:x size) (:x1 selrect))))
delta-y (case snap-v
:top (- (:y1 selrect) (:y size))
:bottom (- (:y2 selrect) (+ (:y size) (/ (:height size) zoom)))
:center (- (/ (- (:height selrect) (/ (:height size) zoom)) 2)
(- (:y size) (:y1 selrect))))]
(gpt/point (* delta-x zoom) (* delta-y zoom))))
(defn- calculate-wrapper
[size1 size2 zoom]
(cond
@ -113,6 +132,10 @@
(mf/with-memo [page overlay zoom]
(calculate-size (:objects page) (:frame overlay) zoom))
delta
(mf/with-memo [size overlay-frame overlay zoom]
(calculate-delta size (:selrect overlay-frame) (:snap-to overlay) zoom))
on-click
(mf/use-fn
(mf/deps overlay close-click-outside?)
@ -145,6 +168,7 @@
:base-frame frame
:frame-offset overlay-position
:size size
:delta delta
:page page
:interactions-mode interactions-mode}]]]))

View file

@ -7,6 +7,7 @@
(ns app.main.ui.viewer.inspect.render
"The main container for a frame in inspect mode"
(:require
[app.common.geom.point :as gpt]
[app.common.geom.shapes :as gsh]
[app.common.pages.helpers :as cph]
[app.main.data.viewer :as dv]
@ -186,7 +187,7 @@
(mf/defc render-frame-svg
[{:keys [page frame local size]}]
(let [objects (mf/with-memo [page frame size]
(prepare-objects frame size (:objects page)))
(prepare-objects frame size (gpt/point 0 0) (:objects page)))
;; Retrieve frame again with correct modifier
frame (get objects (:id frame))

View file

@ -28,9 +28,10 @@
[rumext.v2 :as mf]))
(defn prepare-objects
[frame size objects]
[frame size delta objects]
(let [frame-id (:id frame)
vector (-> (gpt/point (:x size) (:y size))
(gpt/add delta)
(gpt/negate))
update-fn #(d/update-when %1 %2 gsh/transform-shape (ctm/move-modifiers vector))]
(->> (cph/get-children-ids objects frame-id)
@ -46,6 +47,7 @@
base (unchecked-get props "base")
offset (unchecked-get props "offset")
size (unchecked-get props "size")
delta (or (unchecked-get props "delta") (gpt/point 0 0))
vbox (:vbox size)
@ -67,20 +69,26 @@
(map (d/getf (:objects page)))
(concat [frame])
(d/index-by :id)
(prepare-objects frame size)))
(prepare-objects frame size delta)))
wrapper-fixed (mf/with-memo [page frame size]
(shapes/frame-container-factory (calculate-objects fixed-ids)))
objects-fixed (mf/with-memo [fixed-ids page frame size delta]
(calculate-objects fixed-ids))
objects-not-fixed (mf/with-memo [page frame size]
objects-not-fixed (mf/with-memo [not-fixed-ids page frame size delta]
(calculate-objects not-fixed-ids))
all-objects (mf/with-memo [objects-fixed objects-not-fixed]
(merge objects-fixed objects-not-fixed))
wrapper-fixed (mf/with-memo [page frame size]
(shapes/frame-container-factory objects-fixed all-objects))
wrapper-not-fixed (mf/with-memo [objects-not-fixed]
(shapes/frame-container-factory objects-not-fixed))
(shapes/frame-container-factory objects-not-fixed all-objects))
;; Retrieve frames again with correct modifier
frame (get objects-not-fixed (:id frame))
base (get objects-not-fixed (:id base))
frame (get all-objects (:id frame))
base (get all-objects (:id base))
non-delay-interactions (->> (:interactions frame)
(filterv #(not= (:event-type %) :after-delay)))
@ -121,6 +129,7 @@
mode (h/use-equal-memo (unchecked-get props "interactions-mode"))
offset (h/use-equal-memo (unchecked-get props "frame-offset"))
size (h/use-equal-memo (unchecked-get props "size"))
delta (unchecked-get props "delta")
page (unchecked-get props "page")
frame (unchecked-get props "frame")
@ -163,7 +172,8 @@
:frame frame
:base base
:offset offset
:size size}]))
:size size
:delta delta}]))
(mf/defc flows-menu
{::mf/wrap [mf/memo]}

View file

@ -59,19 +59,20 @@
:open-overlay
(let [dest-frame-id (:destination interaction)
viewer-objects (deref (refs/get-viewer-objects))
dest-frame (get viewer-objects dest-frame-id)
dest-frame (get objects dest-frame-id)
relative-to-id (if (= :manual (:overlay-pos-type interaction))
(:id shape) ;; manual interactions are allways from "self"
(if (= (:type shape) :frame) ;; manual interactions are always from "self"
(:frame-id shape)
(:id shape))
(:position-relative-to interaction))
relative-to-shape (or (get objects relative-to-id) base-frame)
close-click-outside (:close-click-outside interaction)
background-overlay (:background-overlay interaction)
overlays-ids (set (map :id overlays))
relative-to-base-frame (find-relative-to-base-frame relative-to-shape objects overlays-ids base-frame)
position (ctsi/calc-overlay-position interaction
[position snap-to] (ctsi/calc-overlay-position interaction
shape
viewer-objects
objects
relative-to-shape
relative-to-base-frame
dest-frame
@ -79,20 +80,23 @@
(when dest-frame-id
(st/emit! (dv/open-overlay dest-frame-id
position
snap-to
close-click-outside
background-overlay
(:animation interaction)))))
:toggle-overlay
(let [frame-id (:destination interaction)
dest-frame (get objects frame-id)
(let [dest-frame-id (:destination interaction)
dest-frame (get objects dest-frame-id)
relative-to-id (if (= :manual (:overlay-pos-type interaction))
(:id shape) ;; manual interactions are allways from "self"
(if (= (:type shape) :frame) ;; manual interactions are always from "self"
(:frame-id shape)
(:id shape))
(:position-relative-to interaction))
relative-to-shape (or (get objects relative-to-id) base-frame)
overlays-ids (set (map :id overlays))
relative-to-base-frame (find-relative-to-base-frame relative-to-shape objects overlays-ids base-frame)
position (ctsi/calc-overlay-position interaction
[position snap-to] (ctsi/calc-overlay-position interaction
shape
objects
relative-to-shape
@ -102,19 +106,21 @@
close-click-outside (:close-click-outside interaction)
background-overlay (:background-overlay interaction)]
(when frame-id
(st/emit! (dv/toggle-overlay frame-id
(when dest-frame-id
(st/emit! (dv/toggle-overlay dest-frame-id
position
snap-to
close-click-outside
background-overlay
(:animation interaction)))))
:close-overlay
(let [frame-id (or (:destination interaction)
(if (= (:type shape) :frame)
(:id shape)
(:frame-id shape)))]
(st/emit! (dv/close-overlay frame-id (:animation interaction))))
(let [dest-frame-id (or (:destination interaction)
(if (and (= (:type shape) :frame)
(some #(= (:id %) (:id shape)) overlays))
(:id shape)
(:frame-id shape)))]
(st/emit! (dv/close-overlay dest-frame-id (:animation interaction))))
:prev-screen
(st/emit! (rt/nav-back-local))
@ -136,29 +142,49 @@
(st/emit! (dv/close-overlay frame-id)))
:toggle-overlay
(let [frame-id (:destination interaction)
position (:overlay-position interaction)
close-click-outside (:close-click-outside interaction)
background-overlay (:background-overlay interaction)]
(when frame-id
(st/emit! (dv/toggle-overlay frame-id
(let [dest-frame-id (:destination interaction)
dest-frame (get objects dest-frame-id)
relative-to-id (if (= :manual (:overlay-pos-type interaction))
(if (= (:type shape) :frame) ;; manual interactions are always from "self"
(:frame-id shape)
(:id shape))
(:position-relative-to interaction))
relative-to-shape (or (get objects relative-to-id) base-frame)
overlays-ids (set (map :id overlays))
relative-to-base-frame (find-relative-to-base-frame relative-to-shape objects overlays-ids base-frame)
[position snap-to] (ctsi/calc-overlay-position interaction
shape
objects
relative-to-shape
relative-to-base-frame
dest-frame
frame-offset)
close-click-outside (:close-click-outside interaction)
background-overlay (:background-overlay interaction)]
(when dest-frame-id
(st/emit! (dv/toggle-overlay dest-frame-id
position
snap-to
close-click-outside
background-overlay
(:animation interaction)))))
:close-overlay
(let [dest-frame-id (:destination interaction)
dest-frame (get objects dest-frame-id)
relative-to-id (if (= :manual (:overlay-pos-type interaction))
(:id shape) ;; manual interactions are allways from "self"
(if (= (:type shape) :frame) ;; manual interactions are always from "self"
(:frame-id shape)
(:id shape))
(:position-relative-to interaction))
relative-to-shape (or (get objects relative-to-id) base-frame)
close-click-outside (:close-click-outside interaction)
background-overlay (:background-overlay interaction)
overlays-ids (set (map :id overlays))
relative-to-base-frame (find-relative-to-base-frame relative-to-shape objects overlays-ids base-frame)
position (ctsi/calc-overlay-position interaction
[position snap-to] (ctsi/calc-overlay-position interaction
shape
objects
relative-to-shape
@ -168,6 +194,7 @@
(when dest-frame-id
(st/emit! (dv/open-overlay dest-frame-id
position
snap-to
close-click-outside
background-overlay
(:animation interaction)))))
@ -258,6 +285,7 @@
childs (unchecked-get props "childs")
frame (unchecked-get props "frame")
objects (unchecked-get props "objects")
all-objects (or (unchecked-get props "all-objects") objects)
base-frame (mf/use-ctx base-frame-ctx)
frame-offset (mf/use-ctx frame-offset-ctx)
interactions-show? (mf/deref viewer-interactions-show?)
@ -266,22 +294,25 @@
svg-element? (and (= :svg-raw (:type shape))
(not= :svg (get-in shape [:content :tag])))
;; The objects parameter has the shapes that we must draw. It may be a subset of
;; all-objects in some cases (e.g. if there are fixed elements). But for interactions
;; handling we need access to all objects inside the page.
on-pointer-down
(mf/use-fn (mf/deps shape base-frame frame-offset objects)
#(on-pointer-down % shape base-frame frame-offset objects overlays))
(mf/use-fn (mf/deps shape base-frame frame-offset all-objects)
#(on-pointer-down % shape base-frame frame-offset all-objects overlays))
on-pointer-up
(mf/use-fn (mf/deps shape base-frame frame-offset objects)
#(on-pointer-up % shape base-frame frame-offset objects overlays))
(mf/use-fn (mf/deps shape base-frame frame-offset all-objects)
#(on-pointer-up % shape base-frame frame-offset all-objects overlays))
on-pointer-enter
(mf/use-fn (mf/deps shape base-frame frame-offset objects)
#(on-pointer-enter % shape base-frame frame-offset objects overlays))
(mf/use-fn (mf/deps shape base-frame frame-offset all-objects)
#(on-pointer-enter % shape base-frame frame-offset all-objects overlays))
on-pointer-leave
(mf/use-fn (mf/deps shape base-frame frame-offset objects)
#(on-pointer-leave % shape base-frame frame-offset objects overlays))]
(mf/use-fn (mf/deps shape base-frame frame-offset all-objects)
#(on-pointer-leave % shape base-frame frame-offset all-objects overlays))]
(mf/with-effect []
(let [sems (on-load shape base-frame frame-offset objects overlays)]
@ -350,8 +381,8 @@
(declare shape-container-factory)
(defn frame-container-factory
[objects]
(let [shape-container (shape-container-factory objects)
[objects all-objects]
(let [shape-container (shape-container-factory objects all-objects)
frame-wrapper (frame-wrapper shape-container)]
(mf/fnc frame-container
{::mf/wrap-props false}
@ -361,13 +392,14 @@
props (obj/merge! #js {} props
#js {:shape shape
:childs childs
:objects objects})]
:objects objects
:all-objects all-objects})]
[:> frame-wrapper props]))))
(defn group-container-factory
[objects]
(let [shape-container (shape-container-factory objects)
[objects all-objects]
(let [shape-container (shape-container-factory objects all-objects)
group-wrapper (group-wrapper shape-container)]
(mf/fnc group-container
{::mf/wrap-props false}
@ -380,8 +412,8 @@
[:> group-wrapper props])))))
(defn bool-container-factory
[objects]
(let [shape-container (shape-container-factory objects)
[objects all-objects]
(let [shape-container (shape-container-factory objects all-objects)
bool-wrapper (bool-wrapper shape-container)]
(mf/fnc bool-container
{::mf/wrap-props false}
@ -394,8 +426,8 @@
[:> bool-wrapper props]))))
(defn svg-raw-container-factory
[objects]
(let [shape-container (shape-container-factory objects)
[objects all-objects]
(let [shape-container (shape-container-factory objects all-objects)
svg-raw-wrapper (svg-raw-wrapper shape-container)]
(mf/fnc svg-raw-container
{::mf/wrap-props false}
@ -407,7 +439,7 @@
[:> svg-raw-wrapper props]))))
(defn shape-container-factory
[objects]
[objects all-objects]
(let [path-wrapper (path-wrapper)
text-wrapper (text-wrapper)
rect-wrapper (rect-wrapper)
@ -422,26 +454,27 @@
group-container
(mf/with-memo [objects]
(group-container-factory objects))
(group-container-factory objects all-objects))
frame-container
(mf/with-memo [objects]
(frame-container-factory objects))
(frame-container-factory objects all-objects))
bool-container
(mf/with-memo [objects]
(bool-container-factory objects))
(bool-container-factory objects all-objects))
svg-raw-container
(mf/with-memo [objects]
(svg-raw-container-factory objects))]
(svg-raw-container-factory objects all-objects))]
(when (and shape (not (:hidden shape)))
(let [shape (-> shape
#_(gsh/transform-shape)
(gsh/translate-to-frame frame))
opts #js {:shape shape
:objects objects}]
:objects objects
:all-objects all-objects}]
(case (:type shape)
:frame [:> frame-container opts]
:text [:> text-wrapper opts]

View file

@ -87,7 +87,7 @@
dw/clear-edition-mode)
;; Delay so anything that launched :interrupt can finish
(st/emit! 100 (dw/select-for-drawing tool)))))
(ts/schedule 100 #(st/emit! (dw/select-for-drawing tool))))))
toggle-text-palette
(mf/use-fn

View file

@ -75,7 +75,7 @@
(d/without-nils {:color (str/lower (dm/get-in shadow [:color :color]))
:opacity (dm/get-in shadow [:color :opacity])
:gradient (dm/get-in shadow [:color :gradient])}))]
{:attrs attrs
:prop :shadow
@ -157,41 +157,33 @@
expand-color (mf/use-state false)
grouped-colors* (mf/use-var nil)
prev-color* (mf/use-var nil)
prev-colors* (mf/use-var [])
on-change
(mf/use-fn
(fn [new-color old-color from-picker?]
(let [old-color (-> old-color
(dissoc :name)
(dissoc :path)
(d/without-nils))
prev-color (when @prev-color*
(-> @prev-color*
(dissoc :name)
(dissoc :path)
(d/without-nils)))
(let [old-color (-> old-color (dissoc :name :path) d/without-nils)
;; When dragging on the color picker sometimes all the shapes hasn't updated the color to the prev value so we need this extra calculation
shapes-by-old-color (get @grouped-colors* old-color)
prev-color (d/seek #(get @grouped-colors* %) @prev-colors*)
shapes-by-prev-color (get @grouped-colors* prev-color)
shapes-by-color (or shapes-by-prev-color shapes-by-old-color)]
(when from-picker?
(reset! prev-color* new-color))
(swap! prev-colors* conj (-> new-color (dissoc :name :path) d/without-nils)))
(st/emit! (dc/change-color-in-selected new-color shapes-by-color (or prev-color old-color))))))
on-open
(mf/use-fn
(fn []
(reset! prev-color* nil)))
(reset! prev-colors* [])))
on-close
(mf/use-fn
(fn []
(reset! prev-color* nil)))
(reset! prev-colors* [])))
on-detach
(mf/use-fn
@ -217,7 +209,7 @@
[:div.element-set-content
[:div.selected-colors
(for [[index color] (d/enumerate (take 3 library-colors))]
[:& color-row {:key (dm/str "library-color-" index)
[:& color-row {:key (dm/str "library-color-" (:color color))
:color color
:index index
:on-detach on-detach
@ -231,7 +223,7 @@
[:span.text (tr "workspace.options.more-lib-colors")]])
(when @expand-lib-color
(for [[index color] (d/enumerate (drop 3 library-colors))]
[:& color-row {:key (dm/str "library-color-" index)
[:& color-row {:key (dm/str "library-color-" (:color color))
:color color
:index index
:on-detach on-detach
@ -255,7 +247,7 @@
[:span.text (tr "workspace.options.more-colors")]])
(when @expand-color
(for [[index color] (d/enumerate (drop 3 colors))]
[:& color-row {:key (dm/str "color-" index)
[:& color-row {:key (dm/str "color-" (:color color))
:color color
:index index
:select-only select-only

View file

@ -315,12 +315,12 @@
[measure-ids measure-values] (get-attrs shapes objects :measure)
[layer-ids layer-values
text-ids text-values
constraint-ids constraint-values
fill-ids fill-values
shadow-ids shadow-values
blur-ids blur-values
stroke-ids stroke-values
text-ids text-values
exports-ids exports-values
layout-container-ids layout-container-values
layout-item-ids layout-item-values]
@ -331,12 +331,12 @@
[]
(mapcat identity)
[(get-attrs shapes objects-no-measures :layer)
(get-attrs shapes objects-no-measures :text)
(get-attrs shapes objects-no-measures :constraint)
(get-attrs shapes objects-no-measures :fill)
(get-attrs shapes objects-no-measures :shadow)
(get-attrs shapes objects-no-measures :blur)
(get-attrs shapes objects-no-measures :stroke)
(get-attrs shapes objects-no-measures :text)
(get-attrs shapes objects-no-measures :exports)
(get-attrs shapes objects-no-measures :layout-container)
(get-attrs shapes objects-no-measures :layout-item)
@ -364,6 +364,9 @@
(when-not (empty? layer-ids)
[:& layer-menu {:type type :ids layer-ids :values layer-values}])
(when-not (empty? text-ids)
[:& ot/text-menu {:type type :ids text-ids :values text-values}])
(when-not (empty? fill-ids)
[:& fill-menu {:type type :ids fill-ids :values fill-values}])
@ -380,8 +383,5 @@
(when-not (empty? blur-ids)
[:& blur-menu {:type type :ids blur-ids :values blur-values}])
(when-not (empty? text-ids)
[:& ot/text-menu {:type type :ids text-ids :values text-values}])
(when-not (empty? exports-ids)
[:& exports-menu {:type type :ids exports-ids :values exports-values :page-id page-id :file-id file-id}])]))

View file

@ -2321,9 +2321,6 @@ msgstr "Přichytit k vodicím lištám"
msgid "shortcuts.toggle-textpalette"
msgstr "Přepnout paletu textu"
msgid "shortcuts.toggle-visibility"
msgstr "Přepnout viditelnost"
msgid "shortcuts.toggle-zoom-style"
msgstr "Přepnout styl přiblížení"

View file

@ -1017,6 +1017,9 @@ msgstr "Email or password is incorrect."
msgid "errors.wrong-old-password"
msgstr "Old password is incorrect"
msgid "errors.cannot-upload"
msgstr "Cannot upload the media file."
#: src/app/main/ui/settings/feedback.cljs
msgid "feedback.description"
msgstr "Description"
@ -2528,7 +2531,7 @@ msgid "shortcuts.h-distribute"
msgstr "Distribute horizontally"
msgid "shortcuts.hide-ui"
msgstr "Show/hide UI"
msgstr "Show / Hide UI"
msgid "shortcuts.increase-zoom"
msgstr "Zoom in"
@ -2687,10 +2690,10 @@ msgid "shortcuts.separate-nodes"
msgstr "Separate nodes"
msgid "shortcuts.show-pixel-grid"
msgstr "Show/hide pixel grid"
msgstr "Show / Hide pixel grid"
msgid "shortcuts.show-shortcuts"
msgstr "Show/hide shortcuts"
msgstr "Show / Hide shortcuts"
msgid "shortcuts.snap-nodes"
msgstr "Snap to nodes"
@ -2742,7 +2745,7 @@ msgid "shortcuts.toggle-fullscreen"
msgstr "Toggle fullscreen"
msgid "shortcuts.toggle-grid"
msgstr "Show/hide grid"
msgstr "Show / Hide grid"
msgid "shortcuts.toggle-history"
msgstr "Toggle history"
@ -2751,16 +2754,16 @@ msgid "shortcuts.toggle-layers"
msgstr "Toggle layers"
msgid "shortcuts.toggle-layout-flex"
msgstr "Add/remove flex layout"
msgstr "Add / Remove flex layout"
msgid "shortcuts.toggle-lock"
msgstr "Lock selected"
msgstr "Lock / Unlock"
msgid "shortcuts.toggle-lock-size"
msgstr "Lock proportions"
msgid "shortcuts.toggle-rules"
msgstr "Show/hide rulers"
msgstr "Show / Hide rulers"
msgid "shortcuts.toggle-scale-text"
msgstr "Toggle scale text"
@ -2775,7 +2778,7 @@ msgid "shortcuts.toggle-textpalette"
msgstr "Toggle text palette"
msgid "shortcuts.toggle-visibility"
msgstr "Toggle visibility"
msgstr "Show / Hide"
msgid "shortcuts.toggle-zoom-style"
msgstr "Toggle zoom style"
@ -4475,7 +4478,7 @@ msgid "workspace.shape.menu.hide"
msgstr "Hide"
msgid "workspace.shape.menu.hide-ui"
msgstr "Show/Hide UI"
msgstr "Show / Hide UI"
msgid "workspace.shape.menu.intersection"
msgstr "Intersection"

View file

@ -1057,6 +1057,9 @@ msgstr "El email o la contraseña son incorrectos."
msgid "errors.wrong-old-password"
msgstr "La contraseña anterior no es correcta"
msgid "errors.cannot-upload"
msgstr "No se puede subir el fichero"
#: src/app/main/ui/settings/feedback.cljs
msgid "feedback.description"
msgstr "Descripción"

View file

@ -2592,9 +2592,6 @@ msgstr "הצמדה לקווים מנחים"
msgid "shortcuts.toggle-textpalette"
msgstr "החלפת לוח טקסט"
msgid "shortcuts.toggle-visibility"
msgstr "החלפת מצב הצגה"
msgid "shortcuts.toggle-zoom-style"
msgstr "החלפת סגנון תקריב"

View file

@ -2405,9 +2405,6 @@ msgstr "Pričvrsti na guides"
msgid "shortcuts.toggle-textpalette"
msgstr "Promijeni paletu teksta"
msgid "shortcuts.toggle-visibility"
msgstr "Promijeni vidljivost"
msgid "shortcuts.toggle-zoom-style"
msgstr "Promijeni stil zooma"

View file

@ -2559,9 +2559,6 @@ msgstr "Tancap ke pemandu"
msgid "shortcuts.toggle-textpalette"
msgstr "Alih palet teks"
msgid "shortcuts.toggle-visibility"
msgstr "Alih keterlihatan"
msgid "shortcuts.toggle-zoom-style"
msgstr "Alih gaya zum"

View file

@ -2592,9 +2592,6 @@ msgstr "Pieķerties vadotnēm"
msgid "shortcuts.toggle-textpalette"
msgstr "Pārslēgt teksta paleti"
msgid "shortcuts.toggle-visibility"
msgstr "Pārslēgt redzamību"
msgid "shortcuts.toggle-zoom-style"
msgstr "Pārslēgt tālummaiņas stilu"

View file

@ -2612,9 +2612,6 @@ msgstr "Przyciągaj do prowadnic"
msgid "shortcuts.toggle-textpalette"
msgstr "Przełącz paletę tekstu"
msgid "shortcuts.toggle-visibility"
msgstr "Przełącz widoczność"
msgid "shortcuts.toggle-zoom-style"
msgstr "Przełącz sposób powiększania"

View file

@ -2567,9 +2567,6 @@ msgstr "Aderir as réguas"
msgid "shortcuts.toggle-textpalette"
msgstr "Mostrar/Esconder paleta de tipografias"
msgid "shortcuts.toggle-visibility"
msgstr "Alternar visibilidade"
msgid "shortcuts.toggle-zoom-style"
msgstr "Alternar estilo de zoom"

View file

@ -2424,9 +2424,6 @@ msgstr "Ajustar às guias"
msgid "shortcuts.toggle-textpalette"
msgstr "Alternar paleta de texto"
msgid "shortcuts.toggle-visibility"
msgstr "Alternar visibilidade"
msgid "shortcuts.toggle-zoom-style"
msgstr "Alternar estilo de zoom"

View file

@ -2589,9 +2589,6 @@ msgstr "Fixare la ghiduri"
msgid "shortcuts.toggle-textpalette"
msgstr "Comutați paleta de text"
msgid "shortcuts.toggle-visibility"
msgstr "Comutați vizibilitatea"
msgid "shortcuts.toggle-zoom-style"
msgstr "Comutați stilul zoomului"

View file

@ -2691,9 +2691,6 @@ msgstr "Kılavuzlara tuttur"
msgid "shortcuts.toggle-textpalette"
msgstr "Metin paletini değiştir"
msgid "shortcuts.toggle-visibility"
msgstr "Görünürlüğü değiştir"
msgid "shortcuts.toggle-zoom-style"
msgstr "Yakınlaştırma şeklini değiştir"

View file

@ -2446,9 +2446,6 @@ msgstr "辅助线对齐"
msgid "shortcuts.toggle-textpalette"
msgstr "切换文本调色板"
msgid "shortcuts.toggle-visibility"
msgstr "切换可见度"
msgid "shortcuts.toggle-zoom-style"
msgstr "切换缩放样式"