diff --git a/CHANGES.md b/CHANGES.md index ba10612e4..db6171c50 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -27,6 +27,13 @@ - To @andrewzhurov for many code contributions on this release. +## 1.15.2-beta + +### :bug: Bugs fixed + +- Fix problem with multi-user text editing [Taiga #3446](https://tree.taiga.io/project/penpot/issue/3446) +- Fix path tools blocking elements underneath [#2050](https://github.com/penpot/penpot/issues/2050) +- Fix frame titles deforming when resize [#2207](https://github.com/penpot/penpot/issues/2207) ## 1.15.1-beta @@ -38,6 +45,7 @@ - Fix validation error on text position [Taiga #4010](https://tree.taiga.io/project/penpot/issue/4010) - Fix objects jitter while scrolling [Github #2167](https://github.com/penpot/penpot/issues/2167) - Fix on color-picker, click+drag adds lots of recent colors [Taiga #4013](https://tree.taiga.io/project/penpot/issue/4013) +- Fix opening profile URL while signed out takes to "your account" section[Taiga #3976](https://tree.taiga.io/project/penpot/issue/3976) ## 1.15.0-beta diff --git a/common/src/app/common/geom/shapes/transforms.cljc b/common/src/app/common/geom/shapes/transforms.cljc index 0b2b49309..f9bda0dc2 100644 --- a/common/src/app/common/geom/shapes/transforms.cljc +++ b/common/src/app/common/geom/shapes/transforms.cljc @@ -160,12 +160,12 @@ ([shape] (transform-str shape nil)) - ([{:keys [transform flip-x flip-y] :as shape} {:keys [no-flip]}] + ([{:keys [transform flip-x flip-y] :as shape} {:keys [no-flip] :as params}] (if (and (some? shape) (or (some? transform) (and (not no-flip) flip-x) (and (not no-flip) flip-y))) - (dm/str (transform-matrix shape)) + (dm/str (transform-matrix shape params)) ""))) (defn inverse-transform-matrix diff --git a/frontend/resources/styles/main/partials/workspace.scss b/frontend/resources/styles/main/partials/workspace.scss index 320c6f8d9..1a1dec905 100644 --- a/frontend/resources/styles/main/partials/workspace.scss +++ b/frontend/resources/styles/main/partials/workspace.scss @@ -352,9 +352,10 @@ $height-palette-max: 80px; position: absolute; width: 100%; z-index: 12; - pointer-events: initial; + pointer-events: none; .path-actions { + pointer-events: initial; display: flex; flex-direction: row; background: white; diff --git a/frontend/src/app/main/data/workspace/notifications.cljs b/frontend/src/app/main/data/workspace/notifications.cljs index a9388898c..cdd7ca26f 100644 --- a/frontend/src/app/main/data/workspace/notifications.cljs +++ b/frontend/src/app/main/data/workspace/notifications.cljs @@ -203,18 +203,24 @@ (fn [{:keys [type attr]}] (and (= :set type) (= attr :position-data))) - remove-update-position-data + add-origin-session-id + (fn [{:keys [] :as op}] + (cond-> op + (position-data-operation? op) + (update :val with-meta {:session-id (:session-id msg)}))) + + update-position-data (fn [change] (cond-> change (= :mod-obj (:type change)) - (update :operations #(filterv (comp not position-data-operation?) %)))) + (update :operations #(mapv add-origin-session-id %)))) process-page-changes (fn [[page-id changes]] (dch/update-indices page-id changes)) - ;; We remove `position-data` from the incomming message - changes (->> changes (mapv remove-update-position-data)) + ;; We update `position-data` from the incomming message + changes (->> changes (mapv update-position-data)) changes-by-pages (group-by :page-id changes)] (rx/merge diff --git a/frontend/src/app/main/ui/settings.cljs b/frontend/src/app/main/ui/settings.cljs index 46edd62c1..334b3feb8 100644 --- a/frontend/src/app/main/ui/settings.cljs +++ b/frontend/src/app/main/ui/settings.cljs @@ -7,14 +7,16 @@ (ns app.main.ui.settings (:require [app.main.refs :as refs] + [app.main.store :as st] [app.main.ui.settings.change-email] [app.main.ui.settings.delete-account] [app.main.ui.settings.feedback :refer [feedback-page]] [app.main.ui.settings.options :refer [options-page]] [app.main.ui.settings.password :refer [password-page]] [app.main.ui.settings.profile :refer [profile-page]] - [app.main.ui.settings.sidebar :refer [sidebar]] - [app.util.i18n :as i18n :refer [tr]] + [app.main.ui.settings.sidebar :refer [sidebar]] + [app.util.i18n :as i18n :refer [tr]] + [app.util.router :as rt] [rumext.alpha :as mf])) (mf/defc header @@ -29,6 +31,10 @@ (let [section (get-in route [:data :name]) profile (mf/deref refs/profile) locale (mf/deref i18n/locale)] + (mf/use-effect + #(when (nil? profile) + (st/emit! (rt/nav :auth-login)))) + [:section.dashboard-layout [:& sidebar {:profile profile :locale locale diff --git a/frontend/src/app/main/ui/workspace/shapes/frame/dynamic_modifiers.cljs b/frontend/src/app/main/ui/workspace/shapes/frame/dynamic_modifiers.cljs index dc46bf241..60bb2a774 100644 --- a/frontend/src/app/main/ui/workspace/shapes/frame/dynamic_modifiers.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/frame/dynamic_modifiers.cljs @@ -11,6 +11,8 @@ [app.common.geom.matrix :as gmt] [app.common.geom.point :as gpt] [app.common.geom.shapes :as gsh] + [app.main.store :as st] + [app.main.ui.workspace.viewport.utils :as vwu] [app.util.dom :as dom] [rumext.alpha :as mf])) @@ -202,6 +204,12 @@ (gsh/transform-matrix {:no-flip true}))] (override-transform-att! node "transform" mtx)))) + (dom/class? node "frame-title") + (let [shape (-> shape (assoc :modifiers modifiers) gsh/transform-shape) + zoom (get-in @st/state [:workspace-local :zoom] 1) + mtx (vwu/title-transform shape zoom)] + (override-transform-att! node "transform" mtx)) + (or (= (dom/get-tag-name node) "mask") (= (dom/get-tag-name node) "filter")) (transform-region! node modifiers) diff --git a/frontend/src/app/main/ui/workspace/shapes/text/editor.cljs b/frontend/src/app/main/ui/workspace/shapes/text/editor.cljs index 1417c5d5b..0e9791dfd 100644 --- a/frontend/src/app/main/ui/workspace/shapes/text/editor.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/text/editor.cljs @@ -268,7 +268,11 @@ text-modifier (mf/deref text-modifier-ref) - bounding-box (gsht/position-data-bounding-box (or text-modifier shape)) + shape (cond-> shape + (some? text-modifier) + (dwt/apply-text-modifier text-modifier)) + + bounding-box (gsht/position-data-bounding-box shape) x (min (:x bounding-box) (:x shape)) y (min (:y bounding-box) (:y shape)) diff --git a/frontend/src/app/main/ui/workspace/shapes/text/viewport_texts.cljs b/frontend/src/app/main/ui/workspace/shapes/text/viewport_texts.cljs deleted file mode 100644 index b0aa93524..000000000 --- a/frontend/src/app/main/ui/workspace/shapes/text/viewport_texts.cljs +++ /dev/null @@ -1,258 +0,0 @@ -;; 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) UXBOX Labs SL - -(ns app.main.ui.workspace.shapes.text.viewport-texts - (:require - [app.common.data :as d] - [app.common.data.macros :as dm] - [app.common.geom.shapes :as gsh] - [app.common.geom.shapes.text :as gsht] - [app.common.math :as mth] - [app.common.pages.helpers :as cph] - [app.common.text :as txt] - [app.main.data.workspace.texts :as dwt] - [app.main.fonts :as fonts] - [app.main.refs :as refs] - [app.main.store :as st] - [app.main.ui.hooks :as hooks] - [app.main.ui.shapes.text.fo-text :as fo] - [app.util.dom :as dom] - [app.util.object :as obj] - [app.util.text-editor :as ted] - [app.util.text-svg-position :as tsp] - [app.util.timers :as ts] - [promesa.core :as p] - [rumext.alpha :as mf])) - -(defn strip-position-data [shape] - (dissoc shape :position-data :transform :transform-inverse)) - -(defn strip-modifier - [modifier] - (if (or (some? (dm/get-in modifier [:modifiers :resize-vector])) - (some? (dm/get-in modifier [:modifiers :resize-vector-2]))) - modifier - (d/update-when modifier :modifiers dissoc :displacement :rotation))) - -(defn process-shape [modifiers {:keys [id] :as shape}] - (let [modifier (-> (get modifiers id) strip-modifier) - shape (cond-> shape - (not (gsh/empty-modifiers? (:modifiers modifier))) - (-> (assoc :grow-type :fixed) - (merge modifier) gsh/transform-shape))] - (-> shape - (cond-> (nil? (:position-data shape)) - (assoc :migrate true)) - strip-position-data))) - -(defn- update-with-editor-state - "Updates the shape with the current state in the editor" - [shape editor-state] - (let [content (:content shape) - editor-content - (when editor-state - (-> editor-state - (ted/get-editor-current-content) - (ted/export-content)))] - - (cond-> shape - (and (some? shape) (some? editor-content)) - (assoc :content (d/txt-merge content editor-content))))) - -(defn- update-text-shape - [{:keys [grow-type id migrate] :as shape} node] - ;; Check if we need to update the size because it's auto-width or auto-height - ;; Update the position-data of every text fragment - (p/let [position-data (tsp/calc-position-data id)] - ;; At least one paragraph needs to be inside the bounding box - (when (gsht/overlaps-position-data? shape position-data) - (st/emit! (dwt/update-position-data id position-data))) - - (when (contains? #{:auto-height :auto-width} grow-type) - (let [{:keys [width height]} - (-> (dom/query node ".paragraph-set") - (dom/get-client-size)) - width (mth/ceil width) - height (mth/ceil height)] - (when (and (not (mth/almost-zero? width)) - (not (mth/almost-zero? height)) - (not migrate)) - (st/emit! (dwt/resize-text id width height))))) - (st/emit! (dwt/clean-text-modifier id)))) - -(defn- update-text-modifier - [{:keys [grow-type id]} node] - (p/let [position-data (tsp/calc-position-data id) - props {:position-data position-data} - - props - (if (contains? #{:auto-height :auto-width} grow-type) - (let [{:keys [width height]} (-> (dom/query node ".paragraph-set") (dom/get-client-size)) - width (mth/ceil width) - height (mth/ceil height)] - (if (and (not (mth/almost-zero? width)) (not (mth/almost-zero? height))) - (assoc props :width width :height height) - props)) - props)] - - (st/emit! (dwt/update-text-modifier id props)))) - -(mf/defc text-container - {::mf/wrap-props false - ::mf/wrap [mf/memo]} - [props] - (let [shape (obj/get props "shape") - on-update (obj/get props "on-update") - - handle-update - (mf/use-callback - (mf/deps shape on-update) - (fn [node] - (when (some? node) - (on-update shape node))))] - - [:& fo/text-shape {:key (str "shape-" (:id shape)) - :ref handle-update - :shape shape - :grow-type (:grow-type shape)}])) - -(mf/defc viewport-texts-wrapper - {::mf/wrap-props false - ::mf/wrap [mf/memo #(mf/deferred % ts/idle-then-raf)]} - [props] - (let [text-shapes (obj/get props "text-shapes") - modifiers (obj/get props "modifiers") - prev-modifiers (hooks/use-previous modifiers) - prev-text-shapes (hooks/use-previous text-shapes) - - ;; A change in position-data won't be a "real" change - text-change? - (fn [id] - (let [old-shape (get prev-text-shapes id) - new-shape (get text-shapes id) - old-modifiers (-> (get prev-modifiers id) strip-modifier) - new-modifiers (-> (get modifiers id) strip-modifier)] - - (or (and (not (identical? old-shape new-shape)) - (not= (dissoc old-shape :migrate :position-data) - (dissoc new-shape :migrate :position-data))) - (not= new-modifiers old-modifiers) - - ;; When the position data is nil we force to recalculate - (:migrate new-shape)))) - - changed-texts - (mf/use-memo - (mf/deps text-shapes modifiers) - #(->> (keys text-shapes) - (filter text-change?) - (map (d/getf text-shapes)))) - - handle-update-modifier (mf/use-callback update-text-modifier) - handle-update-shape (mf/use-callback update-text-shape)] - - [:* - (for [{:keys [id] :as shape} changed-texts] - [:& text-container {:shape (gsh/transform-shape shape) - :on-update (if (some? (get modifiers (:id shape))) - handle-update-modifier - handle-update-shape) - :key (str (dm/str "text-container-" id))}])])) - -(mf/defc viewport-text-editing - {::mf/wrap-props false} - [props] - - (let [shape (obj/get props "shape") - - ;; Join current objects with the state of the editor - editor-state - (-> (mf/deref refs/workspace-editor-state) - (get (:id shape))) - - text-modifier-ref - (mf/use-memo (mf/deps (:id shape)) #(refs/workspace-text-modifier-by-id (:id shape))) - - text-modifier - (mf/deref text-modifier-ref) - - shape (cond-> shape - (some? editor-state) - (update-with-editor-state editor-state)) - - ;; When we have a text with grow-type :auto-height we need to check the correct height - ;; otherwise the center alignment will break - shape - (if (or (not= :auto-height (:grow-type shape)) (empty? text-modifier)) - shape - (let [tr-shape (dwt/apply-text-modifier shape text-modifier)] - (cond-> shape - ;; we only change the height otherwise could cause problems with the other fields - (some? text-modifier) - (assoc :height (:height tr-shape))))) - - shape (hooks/use-equal-memo shape) - - handle-update-shape (mf/use-callback update-text-modifier)] - - (mf/use-effect - (mf/deps (:id shape)) - (fn [] - #(st/emit! (dwt/remove-text-modifier (:id shape))))) - - [:& text-container {:shape shape - :on-update handle-update-shape}])) - -(defn check-props - [new-props old-props] - (and (identical? (unchecked-get new-props "objects") - (unchecked-get old-props "objects")) - (identical? (unchecked-get new-props "modifiers") - (unchecked-get old-props "modifiers")) - (= (unchecked-get new-props "edition") - (unchecked-get old-props "edition")))) - -(mf/defc viewport-texts - {::mf/wrap-props false - ::mf/wrap [#(mf/memo' % check-props)]} - [props] - (let [objects (obj/get props "objects") - edition (obj/get props "edition") - modifiers (obj/get props "modifiers") - - text-shapes - (mf/use-memo - (mf/deps objects) - #(into {} (filter (comp cph/text-shape? second)) objects)) - - text-shapes - (mf/use-memo - (mf/deps text-shapes modifiers) - #(d/update-vals text-shapes (partial process-shape modifiers))) - - editing-shape (get text-shapes edition) - - ;; This memo is necesary so the viewport-text-wrapper memoize its props correctly - text-shapes-wrapper - (mf/use-memo - (mf/deps text-shapes edition) - (fn [] - (dissoc text-shapes edition)))] - - ;; We only need the effect to run on "mount" because the next fonts will be changed when the texts are - ;; edited - (mf/use-effect - (fn [] - (let [text-nodes (->> text-shapes (vals)(mapcat #(txt/node-seq txt/is-text-node? (:content %)))) - fonts (into #{} (keep :font-id) text-nodes)] - (run! fonts/ensure-loaded! fonts)))) - - [:* - (when editing-shape - [:& viewport-text-editing {:shape editing-shape}]) - - [:& viewport-texts-wrapper {:text-shapes text-shapes-wrapper - :modifiers modifiers}]])) diff --git a/frontend/src/app/main/ui/workspace/shapes/text/viewport_texts_html.cljs b/frontend/src/app/main/ui/workspace/shapes/text/viewport_texts_html.cljs index a8892489a..299bf7425 100644 --- a/frontend/src/app/main/ui/workspace/shapes/text/viewport_texts_html.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/text/viewport_texts_html.cljs @@ -28,7 +28,10 @@ [rumext.alpha :as mf])) (defn strip-position-data [shape] - (dissoc shape :position-data :transform :transform-inverse)) + (-> shape + (cond-> (some? (meta (:position-data shape))) + (with-meta (meta (:position-data shape)))) + (dissoc :position-data :transform :transform-inverse))) (defn strip-modifier [modifier] @@ -131,14 +134,17 @@ ;; A change in position-data won't be a "real" change text-change? (fn [id] - (let [old-shape (get prev-text-shapes id) - new-shape (get text-shapes id) + (let [new-shape (get text-shapes id) + old-shape (get prev-text-shapes id) old-modifiers (-> (get prev-modifiers id) strip-modifier) - new-modifiers (-> (get modifiers id) strip-modifier)] + new-modifiers (-> (get modifiers id) strip-modifier) - (or (and (not (identical? old-shape new-shape)) - (not= (dissoc old-shape :migrate :position-data) - (dissoc new-shape :migrate :position-data))) + remote? (some? (-> new-shape meta :session-id)) ] + + (or (and (not remote?) + (not (identical? old-shape new-shape)) + (not= (dissoc old-shape :migrate) + (dissoc new-shape :migrate))) (not= new-modifiers old-modifiers) ;; When the position data is nil we force to recalculate diff --git a/frontend/src/app/main/ui/workspace/viewport/utils.cljs b/frontend/src/app/main/ui/workspace/viewport/utils.cljs index e657b7267..6415c4b9d 100644 --- a/frontend/src/app/main/ui/workspace/viewport/utils.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/utils.cljs @@ -9,6 +9,7 @@ [app.common.data :as d] [app.common.data.macros :as dm] [app.common.geom.point :as gpt] + [app.common.geom.shapes :as gsh] [app.main.ui.cursors :as cur] [app.main.ui.formats :refer [format-number]] [app.util.dom :as dom])) @@ -45,3 +46,18 @@ :zoom-in cur/zoom-in :zooom-out cur/zoom-out cur/pointer-inner)) + +;; Ensure that the label has always the same font +;; size, regardless of zoom +;; https://css-tricks.com/transforms-on-svg-elements/ +(defn text-transform + [{:keys [x y]} zoom] + (let [inv-zoom (/ 1 zoom)] + (str + "scale(" inv-zoom ", " inv-zoom ") " + "translate(" (* zoom x) ", " (* zoom y) ")"))) + +(defn title-transform [frame zoom] + (let [frame-transform (gsh/transform-str frame {:no-flip true}) + label-pos (gpt/point (:x frame) (- (:y frame) (/ 10 zoom)))] + (dm/str frame-transform " " (text-transform label-pos zoom)))) diff --git a/frontend/src/app/main/ui/workspace/viewport/widgets.cljs b/frontend/src/app/main/ui/workspace/viewport/widgets.cljs index 1ae92aa12..8ec2addaa 100644 --- a/frontend/src/app/main/ui/workspace/viewport/widgets.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/widgets.cljs @@ -20,6 +20,7 @@ [app.main.ui.hooks :as hooks] [app.main.ui.icons :as i] [app.main.ui.workspace.viewport.path-actions :refer [path-actions]] + [app.main.ui.workspace.viewport.utils :as vwu] [app.util.dom :as dom] [rumext.alpha :as mf])) @@ -82,25 +83,11 @@ :stroke "rgb(49, 239, 184)" :stroke-width (/ 1 zoom)}}])) -;; Ensure that the label has always the same font -;; size, regardless of zoom -;; https://css-tricks.com/transforms-on-svg-elements/ -(defn text-transform - [{:keys [x y]} zoom] - (let [inv-zoom (/ 1 zoom)] - (str - "scale(" inv-zoom ", " inv-zoom ") " - "translate(" (* zoom x) ", " (* zoom y) ")"))) (mf/defc frame-title {::mf/wrap [mf/memo]} [{:keys [frame selected? zoom show-artboard-names? on-frame-enter on-frame-leave on-frame-select]}] - (let [{:keys [width x y]} frame - label-pos (gpt/point x (- y (/ 10 zoom))) - - frame-transform (gsh/transform-str frame) - - on-mouse-down + (let [on-mouse-down (mf/use-callback (mf/deps (:id frame) on-frame-select) (fn [bevent] @@ -140,23 +127,22 @@ text-pos-x (if (:use-for-thumbnail? frame) 15 0)] (when (not (:hidden frame)) - [:g {:id (dm/str "frame-title-" (:id frame))} + [:g.frame-title {:id (dm/str "frame-title-" (:id frame)) :transform (vwu/title-transform frame zoom)} (when (:use-for-thumbnail? frame) - [:g {:transform (dm/str frame-transform " " (text-transform label-pos zoom))} - [:svg {:x 0 - :y -9 - :width 12 - :height 12 - :class "workspace-frame-icon" - :style {:fill (when selected? "var(--color-primary-dark)")} - :visibility (if show-artboard-names? "visible" "hidden")} - [:use {:href "#icon-set-thumbnail"}]]]) + [:svg {:x 0 + :y -9 + :width 12 + :height 12 + :class "workspace-frame-icon" + :style {:fill (when selected? "var(--color-primary-dark)")} + :visibility (if show-artboard-names? "visible" "hidden")} + [:use {:href "#icon-set-thumbnail"}]]) [:text {:x text-pos-x :y 0 - :width width + :width (:width frame) :height 20 :class "workspace-frame-label" - :transform (dm/str frame-transform " " (text-transform label-pos zoom)) + ;:transform (dm/str frame-transform " " (text-transform label-pos zoom)) :style {:fill (when selected? "var(--color-primary-dark)")} :visibility (if show-artboard-names? "visible" "hidden") :on-mouse-down on-mouse-down @@ -172,7 +158,6 @@ [props] (let [objects (unchecked-get props "objects") zoom (unchecked-get props "zoom") - modifiers (unchecked-get props "modifiers") selected (or (unchecked-get props "selected") #{}) show-artboard-names? (unchecked-get props "show-artboard-names?") on-frame-enter (unchecked-get props "on-frame-enter") @@ -188,7 +173,6 @@ :selected? (contains? selected (:id frame)) :zoom zoom :show-artboard-names? show-artboard-names? - :modifiers modifiers :on-frame-enter on-frame-enter :on-frame-leave on-frame-leave :on-frame-select on-frame-select}]))])) @@ -231,7 +215,7 @@ :height 24 :transform (str (when (and selected? modifiers) (str (:displacement modifiers) " " )) - (text-transform flow-pos zoom))} + (vwu/text-transform flow-pos zoom))} [:div.flow-badge {:class (dom/classnames :selected selected?)} [:div.content {:on-mouse-down on-mouse-down :on-double-click on-double-click