diff --git a/CHANGES.md b/CHANGES.md index cf44345654..8d6bdee065 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -56,7 +56,20 @@ - Fix validation on team name input [Taiga #5510](https://tree.taiga.io/project/penpot/issue/5510) - Fix incorrect uri generation issues on share-link modal [Taiga #5564](https://tree.taiga.io/project/penpot/issue/5564) - Fix cache issues with share-links [Taiga #5559](https://tree.taiga.io/project/penpot/issue/5559) - +- Makes height priority for the rows/columns grids [#2774](https://github.com/penpot/penpot/issues/2774) +- Fix problem with comments mode not staying [#3363](https://github.com/penpot/penpot/issues/3363) +- Fix problem with comments when user left the team [Taiga #5562](https://tree.taiga.io/project/penpot/issue/5562) +- Fix problem with images patterns repeating [#3372](https://github.com/penpot/penpot/issues/3372) +- Fix grid not being clipped in frames [#3365](https://github.com/penpot/penpot/issues/3365) +- Fix cut/delete text layer when while creating text [Taiga #5602](https://tree.taiga.io/project/penpot/issue/5602) +- Fix picking a gradient color in recent colors for a new color in the assets tab [Taiga #5601](https://tree.taiga.io/project/penpot/issue/5601) +- Fix problem with importation process [Taiga #5597](https://tree.taiga.io/project/penpot/issue/5597) +- Fix problem with HSV color picker [#3317](https://github.com/penpot/penpot/issues/3317) +- Fix problem with slashes in layers names for exporter [#3276](https://github.com/penpot/penpot/issues/3276) +- Fix incorrect modified data on moving files on dashboard [Taiga #5530](https://tree.taiga.io/project/penpot/issue/5530) +- 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) ### :arrow_up: Deps updates diff --git a/backend/src/app/http/middleware.clj b/backend/src/app/http/middleware.clj index 64f2b5d548..e11a60d04d 100644 --- a/backend/src/app/http/middleware.clj +++ b/backend/src/app/http/middleware.clj @@ -118,8 +118,7 @@ (t/write! tw data))) (catch java.io.IOException _) (catch Throwable cause - (l/warn :hint "unexpected error on encoding response" - :cause cause)) + (l/error :hint "unexpected error on encoding response" :cause cause)) (finally (.close ^OutputStream output-stream)))))) @@ -132,8 +131,8 @@ (catch java.io.IOException _) (catch Throwable cause - (l/warn :hint "unexpected error on encoding response" - :cause cause)) + (l/error :hint "unexpected error on encoding response" + :cause cause)) (finally (.close ^OutputStream output-stream)))))) diff --git a/backend/src/app/rpc/commands/auth.clj b/backend/src/app/rpc/commands/auth.clj index 7281cfed25..d765a5598e 100644 --- a/backend/src/app/rpc/commands/auth.clj +++ b/backend/src/app/rpc/commands/auth.clj @@ -33,7 +33,7 @@ [::sm/word-string {:max 500}]) (def schema:token - [::sm/word-string {:max 1000}]) + [::sm/word-string {:max 6000}]) ;; ---- COMMAND: login with password @@ -323,9 +323,9 @@ :extra-data ptoken}))) (defn register-profile - [{:keys [::db/conn] :as cfg} {:keys [token] :as params}] + [{:keys [::db/conn] :as cfg} {:keys [token fullname] :as params}] (let [claims (tokens/verify (::main/props cfg) {:token token :iss :prepared-register}) - params (merge params claims) + params (assoc claims :fullname fullname) is-active (or (:is-active params) (not (contains? cf/flags :email-verification))) diff --git a/backend/src/app/rpc/commands/binfile.clj b/backend/src/app/rpc/commands/binfile.clj index c962bc2f53..e485fa44ba 100644 --- a/backend/src/app/rpc/commands/binfile.clj +++ b/backend/src/app/rpc/commands/binfile.clj @@ -929,5 +929,10 @@ ::input (:path file) ::project-id project-id ::ignore-index-errors? true))] + + (db/update! conn :project + {:modified-at (dt/now)} + {:id project-id}) + (rph/with-meta ids {::audit/props {:file nil :file-ids ids}})))) diff --git a/backend/src/app/rpc/commands/files.clj b/backend/src/app/rpc/commands/files.clj index c44c13aa46..7eeaaa7561 100644 --- a/backend/src/app/rpc/commands/files.clj +++ b/backend/src/app/rpc/commands/files.clj @@ -189,6 +189,8 @@ ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; (defn check-features-compatibility! + "Function responsible to check if provided features are supported by + the current backend" [features] (let [not-supported (set/difference features supported-features)] (when (seq not-supported) @@ -248,47 +250,59 @@ (into #{} (comp (filter pmap/pointer-map?) (map pmap/get-id))))) +;; FIXME: file locking +(defn- process-components-v2-feature + "A special case handling of the components/v2 feature." + [conn {:keys [id features data] :as file}] + (binding [pmap/*tracked* (atom {})] + (let [data (ctf/migrate-to-components-v2 data) + features (conj features "components/v2") + features' (db/create-array conn "text" features)] + (db/update! conn :file + {:data (blob/encode data) + :features features'} + {:id id}) + (persist-pointers! conn id) + (-> file + (assoc :features features) + (assoc :data data))))) + +(defn handle-file-features! + [conn {:keys [features] :as file} client-features] + + ;; Check features compatibility between the currently supported features on + ;; the current backend instance and the file retrieved from the database + (check-features-compatibility! features) + + (cond-> file + (and (contains? features "components/v2") + (not (contains? client-features "components/v2"))) + (as-> file (ex/raise :type :restriction + :code :feature-mismatch + :feature "components/v2" + :hint "file has 'components/v2' feature enabled but frontend didn't specifies it" + :file-id (:id file))) + + ;; This operation is needed because the components migration generates a new + ;; page with random id which is returned to the client; without persisting + ;; the migration this can cause that two simultaneous clients can have a + ;; different view of the file data and end persisting two pages with main + ;; components and breaking the whole file." + (and (contains? client-features "components/v2") + (not (contains? features "components/v2"))) + (as-> file (process-components-v2-feature conn file)) + + ;; This operation is needed for backward comapatibility with frontends that + ;; does not support pointer-map resolution mechanism; this just resolves the + ;; pointers on backend and return a complete file. + (and (contains? features "storage/pointer-map") + (not (contains? client-features "storage/pointer-map"))) + (process-pointers deref))) + ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; QUERY COMMANDS ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -(defn handle-file-features! - [conn {:keys [id features data] :as file} client-features] - - (when (and (contains? features "components/v2") - (not (contains? client-features "components/v2"))) - (ex/raise :type :restriction - :code :feature-mismatch - :feature "components/v2" - :hint "file has 'components/v2' feature enabled but frontend didn't specifies it")) - - ;; NOTE: this operation is needed because the components migration - ;; generates a new page with random id which is returned to the - ;; client; without persisting the migration this can cause that two - ;; simultaneous clients can have a different view of the file data - ;; and end persisting two pages with main components and breaking - ;; the whole file - (let [file (if (and (contains? client-features "components/v2") - (not (contains? features "components/v2"))) - (binding [pmap/*tracked* (atom {})] - (let [data (ctf/migrate-to-components-v2 data) - features (conj features "components/v2") - features' (db/create-array conn "text" features)] - (db/update! conn :file - {:data (blob/encode data) - :features features'} - {:id id}) - (persist-pointers! conn id) - (-> file - (assoc :features features) - (assoc :data data)))) - file)] - - (cond-> file - (and (contains? features "storage/pointer-map") - (not (contains? client-features "storage/pointer-map"))) - (process-pointers deref)))) - ;; --- COMMAND QUERY: get-file (by id) (sm/def! ::features @@ -331,7 +345,7 @@ ([conn id client-features] (get-file conn id client-features nil)) ([conn id client-features project-id] - ;; here we check if client requested features are supported + ;; here we check if client requested features are supported (check-features-compatibility! client-features) (binding [pmap/*load-fn* (partial load-pointer conn id)] (let [params (merge {:id id} diff --git a/backend/src/app/rpc/commands/files_temp.clj b/backend/src/app/rpc/commands/files_temp.clj index 4eb24cca26..85902a0faf 100644 --- a/backend/src/app/rpc/commands/files_temp.clj +++ b/backend/src/app/rpc/commands/files_temp.clj @@ -86,16 +86,16 @@ (ex/raise :type :validation :code :cant-persist-already-persisted-file)) - (loop [revs (seq revs) - data (blob/decode (:data file))] - (if-let [rev (first revs)] - (recur (rest revs) - (->> rev :changes blob/decode (cp/process-changes data))) - (db/update! conn :file - {:deleted-at nil - :revn revn - :data (blob/encode data)} - {:id id}))) + + (let [data + (->> revs + (mapcat #(->> % :changes blob/decode)) + (cp/process-changes (blob/decode (:data file))))] + (db/update! conn :file + {:deleted-at nil + :revn revn + :data (blob/encode data)} + {:id id})) nil)) (s/def ::persist-temp-file diff --git a/common/src/app/common/fressian.clj b/common/src/app/common/fressian.clj index 1b78fbcb81..f42f9130ea 100644 --- a/common/src/app/common/fressian.clj +++ b/common/src/app/common/fressian.clj @@ -323,3 +323,13 @@ :rfn (fn [^Reader rdr] (let [^List x (read-object! rdr)] (Matrix. (.get x 0) (.get x 1) (.get x 2) (.get x 3) (.get x 4) (.get x 5))))}) + + +;; Backward compatibility for 1.19 with v1.20; + +(add-handlers! + {:name "penpot/geom/rect" + :rfn read-map-like} + {:name "penpot/shape" + :rfn read-map-like}) + diff --git a/common/src/app/common/geom/shapes/rect.cljc b/common/src/app/common/geom/shapes/rect.cljc index 258ed0cd38..c304b99f47 100644 --- a/common/src/app/common/geom/shapes/rect.cljc +++ b/common/src/app/common/geom/shapes/rect.cljc @@ -218,7 +218,7 @@ (make-selrect (min xp1 xp2) (min yp1 yp2) (abs (- xp1 xp2)) (abs (- yp1 yp2))))) (defn clip-selrect - [{:keys [x1 y1 x2 y2] :as sr} bounds] + [{:keys [x1 y1 x2 y2] :as sr} clip-rect] (when (some? sr) - (let [{bx1 :x1 by1 :y1 bx2 :x2 by2 :y2} (rect->selrect bounds)] + (let [{bx1 :x1 by1 :y1 bx2 :x2 by2 :y2 :as sr2} (rect->selrect clip-rect)] (corners->selrect (max bx1 x1) (max by1 y1) (min bx2 x2) (min by2 y2))))) diff --git a/common/src/app/common/types/container.cljc b/common/src/app/common/types/container.cljc index a2c65c6361..1cb7f98bf7 100644 --- a/common/src/app/common/types/container.cljc +++ b/common/src/app/common/types/container.cljc @@ -96,22 +96,25 @@ "Get the parent shape linked to a component for this shape, if any" ([objects shape] (get-component-shape objects shape nil)) ([objects shape {:keys [allow-main?] :or {allow-main? false} :as options}] - (cond - (nil? shape) - nil + (cond + (nil? shape) + nil - (and (not (ctk/in-component-copy? shape)) (not allow-main?)) - nil + (= uuid/zero (:id shape)) + nil - (ctk/instance-root? shape) - shape + (and (not (ctk/in-component-copy? shape)) (not allow-main?)) + nil - :else - (get-component-shape objects (get objects (:parent-id shape)) options)))) + (ctk/instance-root? shape) + shape + + :else + (get-component-shape objects (get objects (:parent-id shape)) options)))) (defn in-component-main? "Check if the shape is inside a component non-main instance. - + Note that we must iterate on the parents because non-root shapes in a main component have not any discriminating attribute." [objects shape] diff --git a/docker/devenv/files/nginx.conf b/docker/devenv/files/nginx.conf index 9725167f8a..ec9051d094 100644 --- a/docker/devenv/files/nginx.conf +++ b/docker/devenv/files/nginx.conf @@ -92,16 +92,6 @@ http { error_page 301 302 307 = @handle_redirect; } - location ~ ^/github/penpot-files/(?[a-zA-Z0-9\-\_\.]+) { - proxy_pass https://raw.githubusercontent.com/penpot/penpot-files/main/$template_file; - proxy_hide_header Access-Control-Allow-Origin; - proxy_set_header User-Agent "curl/7.74.0"; - proxy_set_header Host "raw.githubusercontent.com"; - proxy_set_header Accept "*/*"; - add_header Access-Control-Allow-Origin $http_origin; - proxy_buffering off; - } - location /internal/gfonts/css { proxy_pass https://fonts.googleapis.com/css?$args; proxy_hide_header Access-Control-Allow-Origin; @@ -124,31 +114,6 @@ http { add_header X-Cache-Status $upstream_cache_status; } - location ~ ^/internal/gfonts/font/(?.+) { - proxy_pass https://fonts.gstatic.com/s/$font_file; - - proxy_hide_header Access-Control-Allow-Origin; - proxy_hide_header Cross-Origin-Resource-Policy; - proxy_hide_header Link; - proxy_hide_header Alt-Svc; - proxy_hide_header Cache-Control; - proxy_hide_header Expires; - proxy_hide_header Cross-Origin-Opener-Policy; - proxy_hide_header Report-To; - - proxy_ignore_headers Set-Cookie Vary Cache-Control Expires; - - proxy_set_header User-Agent "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.0.0 Safari/537.36"; - proxy_set_header Host "fonts.gstatic.com"; - proxy_set_header Accept "*/*"; - - proxy_cache penpot; - - add_header Access-Control-Allow-Origin $http_origin; - add_header Cache-Control max-age=86400; - add_header X-Cache-Status $upstream_cache_status; - } - location /internal/assets { internal; alias /home/penpot/penpot/backend/assets; @@ -192,6 +157,41 @@ http { } location / { + location ~ ^/github/penpot-files/(?[a-zA-Z0-9\-\_\.]+) { + proxy_pass https://raw.githubusercontent.com/penpot/penpot-files/main/$template_file; + proxy_hide_header Access-Control-Allow-Origin; + proxy_set_header User-Agent "curl/7.74.0"; + proxy_set_header Host "raw.githubusercontent.com"; + proxy_set_header Accept "*/*"; + add_header Access-Control-Allow-Origin $http_origin; + proxy_buffering off; + } + + location ~ ^/internal/gfonts/font/(?.+) { + proxy_pass https://fonts.gstatic.com/s/$font_file; + + proxy_hide_header Access-Control-Allow-Origin; + proxy_hide_header Cross-Origin-Resource-Policy; + proxy_hide_header Link; + proxy_hide_header Alt-Svc; + proxy_hide_header Cache-Control; + proxy_hide_header Expires; + proxy_hide_header Cross-Origin-Opener-Policy; + proxy_hide_header Report-To; + + proxy_ignore_headers Set-Cookie Vary Cache-Control Expires; + + proxy_set_header User-Agent "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.0.0 Safari/537.36"; + proxy_set_header Host "fonts.gstatic.com"; + proxy_set_header Accept "*/*"; + + proxy_cache penpot; + + add_header Access-Control-Allow-Origin $http_origin; + add_header Cache-Control max-age=86400; + add_header X-Cache-Status $upstream_cache_status; + } + location ~ ^/(/|css|fonts|images|js|wasm) { } diff --git a/docker/images/files/nginx.conf b/docker/images/files/nginx.conf index 30691f8b09..b7cccedc23 100644 --- a/docker/images/files/nginx.conf +++ b/docker/images/files/nginx.conf @@ -115,31 +115,6 @@ http { add_header X-Cache-Status $upstream_cache_status; } - location ~ ^/internal/gfonts/font/(?.+) { - proxy_pass https://fonts.gstatic.com/s/$font_file; - - proxy_hide_header Access-Control-Allow-Origin; - proxy_hide_header Cross-Origin-Resource-Policy; - proxy_hide_header Link; - proxy_hide_header Alt-Svc; - proxy_hide_header Cache-Control; - proxy_hide_header Expires; - proxy_hide_header Cross-Origin-Opener-Policy; - proxy_hide_header Report-To; - - proxy_ignore_headers Set-Cookie Vary Cache-Control Expires; - - proxy_set_header User-Agent "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.0.0 Safari/537.36"; - proxy_set_header Host "fonts.gstatic.com"; - proxy_set_header Accept "*/*"; - - proxy_cache penpot; - - add_header Access-Control-Allow-Origin $http_origin; - add_header Cache-Control max-age=86400; - add_header X-Cache-Status $upstream_cache_status; - } - location /internal/assets { internal; alias /opt/data/assets; @@ -161,6 +136,31 @@ http { } location / { + location ~ ^/internal/gfonts/font/(?.+) { + proxy_pass https://fonts.gstatic.com/s/$font_file; + + proxy_hide_header Access-Control-Allow-Origin; + proxy_hide_header Cross-Origin-Resource-Policy; + proxy_hide_header Link; + proxy_hide_header Alt-Svc; + proxy_hide_header Cache-Control; + proxy_hide_header Expires; + proxy_hide_header Cross-Origin-Opener-Policy; + proxy_hide_header Report-To; + + proxy_ignore_headers Set-Cookie Vary Cache-Control Expires; + + proxy_set_header User-Agent "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.0.0 Safari/537.36"; + proxy_set_header Host "fonts.gstatic.com"; + proxy_set_header Accept "*/*"; + + proxy_cache penpot; + + add_header Access-Control-Allow-Origin $http_origin; + add_header Cache-Control max-age=86400; + add_header X-Cache-Status $upstream_cache_status; + } + location ~* \.(js|css).*$ { add_header Cache-Control "max-age=86400" always; # 24 hours } diff --git a/exporter/src/app/handlers/export_shapes.cljs b/exporter/src/app/handlers/export_shapes.cljs index a8e108ae46..580a274604 100644 --- a/exporter/src/app/handlers/export_shapes.cljs +++ b/exporter/src/app/handlers/export_shapes.cljs @@ -23,6 +23,9 @@ (declare ^:private assoc-file-name) (declare prepare-exports) +;; Regex to clean namefiles +(def sanitize-file-regex #"[\\/:*?\"<>|]") + (s/def ::file-id ::us/uuid) (s/def ::filename ::us/string) (s/def ::name ::us/string) @@ -134,7 +137,7 @@ :on-progress on-progress) append (fn [{:keys [filename path] :as object}] - (rsc/add-to-zip! zip path filename)) + (rsc/add-to-zip! zip path (str/replace filename sanitize-file-regex "_"))) proc (-> (p/do (p/loop [exports (seq exports)] @@ -144,9 +147,7 @@ (p/recur (rest exports))))) (.finalize zip)) (p/then (constantly resource)) - (p/catch on-error)) - ] - + (p/catch on-error))] (if wait (p/then proc #(assoc exchange :response/body (dissoc % :path))) (assoc exchange :response/body (dissoc resource :path))))) diff --git a/frontend/resources/styles/main/partials/colorpicker.scss b/frontend/resources/styles/main/partials/colorpicker.scss index 7930beac20..c26571b9c1 100644 --- a/frontend/resources/styles/main/partials/colorpicker.scss +++ b/frontend/resources/styles/main/partials/colorpicker.scss @@ -199,7 +199,7 @@ } &.value { - background: linear-gradient(var(--gradient-direction), #fff 0%, #000 100%); + background: linear-gradient(var(--gradient-direction), #000 0%, #fff 100%); } .handler { diff --git a/frontend/src/app/main/data/comments.cljs b/frontend/src/app/main/data/comments.cljs index e85e04f3ac..65f4091784 100644 --- a/frontend/src/app/main/data/comments.cljs +++ b/frontend/src/app/main/data/comments.cljs @@ -134,14 +134,13 @@ (rx/throw {:type :comment-error}))))))))) (defn update-comment-thread-status - [{:keys [id] :as thread}] - (dm/assert! (comment-thread? thread)) + [thread-id] (ptk/reify ::update-comment-thread-status ptk/WatchEvent (watch [_ state _] - (let [done #(d/update-in-when % [:comment-threads id] assoc :count-unread-comments 0) + (let [done #(d/update-in-when % [:comment-threads thread-id] assoc :count-unread-comments 0) share-id (-> state :viewer-local :share-id)] - (->> (rp/cmd! :update-comment-thread-status {:id id :share-id share-id}) + (->> (rp/cmd! :update-comment-thread-status {:id thread-id :share-id share-id}) (rx/map (constantly done)) (rx/catch #(rx/throw {:type :comment-error}))))))) @@ -282,7 +281,7 @@ (fetched [[users comments] state] (let [state (-> state (assoc :comment-threads (d/index-by :id comments)) - (assoc :current-file-comments-users (d/index-by :id users)))] + (update :current-file-comments-users merge (d/index-by :id users)))] (reduce set-comment-threds state comments)))] (ptk/reify ::retrieve-comment-threads diff --git a/frontend/src/app/main/data/dashboard.cljs b/frontend/src/app/main/data/dashboard.cljs index 4b61346651..0995fefeba 100644 --- a/frontend/src/app/main/data/dashboard.cljs +++ b/frontend/src/app/main/data/dashboard.cljs @@ -10,6 +10,7 @@ [app.common.data.macros :as dm] [app.common.pages :as cp] [app.common.schema :as sm] + [app.common.time :as dt] [app.common.uri :as u] [app.common.uuid :as uuid] [app.config :as cf] @@ -872,10 +873,14 @@ ptk/UpdateEvent (update [_ state] - (let [origin-project (get-in state [:dashboard-files (first ids) :project-id])] + (let [origin-project (get-in state [:dashboard-files (first ids) :project-id]) + update-project (fn [project] + (-> project + (update :count #(+ % (count ids))) + (assoc :modified-at (dt/now))))] (-> state - (d/update-in-when [:dashboard-projects origin-project] update :count #(- % (count ids))) - (d/update-in-when [:dashboard-projects project-id] update :count #(+ % (count ids)))))) + (d/update-in-when [:dashboard-projects origin-project] update-project) + (d/update-in-when [:dashboard-projects project-id] update-project)))) ptk/WatchEvent (watch [_ _ _] diff --git a/frontend/src/app/main/data/workspace/colors.cljs b/frontend/src/app/main/data/workspace/colors.cljs index 0d460babbb..fbd330b2dd 100644 --- a/frontend/src/app/main/data/workspace/colors.cljs +++ b/frontend/src/app/main/data/workspace/colors.cljs @@ -515,27 +515,32 @@ (let [shape-id (-> state wsh/lookup-selected first)] (update state :colorpicker (fn [state] - (if (some? gradient) - (let [stop (or (:editing-stop state) 0) - stops (mapv split-color-components (:stops gradient)) - type (case (:type gradient) - :linear :linear-gradient - :radial :radial-gradient)] - (-> state - (assoc :type type) - (assoc :current-color (nth stops stop)) - (assoc :stops stops) - (assoc :gradient (-> gradient - (dissoc :stops) - (assoc :shape-id shape-id))) - (assoc :editing-stop stop))) + (let [current-color (:current-color state)] + (if (some? gradient) + (let [stop (or (:editing-stop state) 0) + stops (mapv split-color-components (:stops gradient)) + type (case (:type gradient) + :linear :linear-gradient + :radial :radial-gradient + (:type state))] + (-> state + (assoc :type type) + (assoc :current-color (nth stops stop)) + (assoc :stops stops) + (assoc :gradient (-> gradient + (dissoc :stops) + (assoc :shape-id shape-id))) + (assoc :editing-stop stop))) - (-> state - (assoc :type :color) - (assoc :current-color (split-color-components (dissoc data :gradient))) - (dissoc :editing-stop) - (dissoc :gradient) - (dissoc :stops))))))))) + (-> state + (assoc :type :color) + (cond-> (or (nil? current-color) + (not= (:color data) (:color current-color)) + (not= (:opacity data) (:opacity current-color))) + (assoc :current-color (split-color-components (dissoc data :gradient)))) + (dissoc :editing-stop) + (dissoc :gradient) + (dissoc :stops)))))))))) (defn update-colorpicker-color [changes add-recent?] diff --git a/frontend/src/app/main/data/workspace/texts.cljs b/frontend/src/app/main/data/workspace/texts.cljs index a9a3f4fe70..eea3ec6c81 100644 --- a/frontend/src/app/main/data/workspace/texts.cljs +++ b/frontend/src/app/main/data/workspace/texts.cljs @@ -142,14 +142,17 @@ (rx/merge (rx/of (update-editor-state shape nil)) (when (and (not= content (:content shape)) - (some? (:current-page-id state))) + (some? (:current-page-id state)) + (some? shape)) (rx/of (dch/update-shapes [id] (fn [shape] - (let [{:keys [width height]} modifiers] + (let [{:keys [width height position-data]} modifiers] (-> shape (assoc :content content) + (cond-> position-data + (assoc :position-data position-data)) (cond-> new-shape? (assoc :name text)) (cond-> (or (some? width) (some? height)) diff --git a/frontend/src/app/main/errors.cljs b/frontend/src/app/main/errors.cljs index 6351192340..ea60fa7b85 100644 --- a/frontend/src/app/main/errors.cljs +++ b/frontend/src/app/main/errors.cljs @@ -165,12 +165,22 @@ [{:keys [code] :as error}] (cond (= :feature-mismatch code) - (let [message (tr "errors.feature-mismatch" (:feature error))] - (st/emit! (modal/show {:type :alert :message message}))) + (let [message (tr "errors.feature-mismatch" (:feature error)) + team-id (:current-team-id @st/state) + project-id (:current-project-id @st/state) + on-accept #(if (and project-id team-id) + (st/emit! (rt/nav :dashboard-files {:team-id team-id :project-id project-id})) + (set! (.-href glob/location) ""))] + (st/emit! (modal/show {:type :alert :message message :on-accept on-accept}))) (= :features-not-supported code) - (let [message (tr "errors.feature-not-supported" (:feature error))] - (st/emit! (modal/show {:type :alert :message message}))) + (let [message (tr "errors.feature-not-supported" (:feature error)) + team-id (:current-team-id @st/state) + project-id (:current-project-id @st/state) + on-accept #(if (and project-id team-id) + (st/emit! (rt/nav :dashboard-files {:team-id team-id :project-id project-id})) + (set! (.-href glob/location) ""))] + (st/emit! (modal/show {:type :alert :message message :on-accept on-accept}))) (= :max-quote-reached code) (let [message (tr "errors.max-quote-reached" (:target error))] diff --git a/frontend/src/app/main/refs.cljs b/frontend/src/app/main/refs.cljs index 92190f0311..bcb78e7a4a 100644 --- a/frontend/src/app/main/refs.cljs +++ b/frontend/src/app/main/refs.cljs @@ -298,6 +298,15 @@ (into [] (keep #(get objects %)) parent-ids))) workspace-page-objects =)) +(defn shape-parents + [id] + (l/derived + (fn [objects] + (into [] + (keep (d/getf objects)) + (cph/get-parent-ids objects id))) + workspace-page-objects =)) + (defn children-objects [id] (l/derived diff --git a/frontend/src/app/main/ui/alert.cljs b/frontend/src/app/main/ui/alert.cljs index 29e4d9393c..1acb30eb88 100644 --- a/frontend/src/app/main/ui/alert.cljs +++ b/frontend/src/app/main/ui/alert.cljs @@ -10,11 +10,10 @@ [app.main.store :as st] [app.main.ui.icons :as i] [app.util.dom :as dom] - [app.util.i18n :as i18n :refer [tr t]] + [app.util.i18n :as i18n :refer [tr]] [app.util.keyboard :as k] [goog.events :as events] - [rumext.v2 :as mf]) - (:import goog.events.EventType)) + [rumext.v2 :as mf])) (mf/defc alert-dialog {::mf/register modal/components @@ -26,29 +25,27 @@ hint accept-label accept-style] :as props}] - (let [locale (mf/deref i18n/locale) - - on-accept (or on-accept identity) - message (or message (t locale "ds.alert-title")) + (let [on-accept (or on-accept identity) + message (or message (tr "ds.alert-title")) accept-label (or accept-label (tr "ds.alert-ok")) accept-style (or accept-style :danger) - title (or title (t locale "ds.alert-title")) + title (or title (tr "ds.alert-title")) accept-fn - (mf/use-callback + (mf/use-fn (fn [event] (dom/prevent-default event) (st/emit! (modal/hide)) (on-accept props)))] - (mf/with-effect + (mf/with-effect [] (letfn [(on-keydown [event] (when (k/enter? event) (dom/prevent-default event) (dom/stop-propagation event) (st/emit! (modal/hide)) (on-accept props)))] - (->> (events/listen js/document EventType.KEYDOWN on-keydown) + (->> (events/listen js/document "keydown" on-keydown) (partial events/unlistenByKey)))) [:div.modal-overlay diff --git a/frontend/src/app/main/ui/comments.cljs b/frontend/src/app/main/ui/comments.cljs index c83cf0310b..7adcede2d6 100644 --- a/frontend/src/app/main/ui/comments.cljs +++ b/frontend/src/app/main/ui/comments.cljs @@ -6,6 +6,7 @@ (ns app.main.ui.comments (:require + [app.common.data :as d] [app.common.data.macros :as dm] [app.common.geom.point :as gpt] [app.config :as cfg] @@ -19,38 +20,55 @@ [app.util.dom :as dom] [app.util.i18n :as i18n :refer [tr]] [app.util.keyboard :as kbd] - [app.util.object :as obj] [app.util.time :as dt] [cuerdas.core :as str] [okulary.core :as l] [rumext.v2 :as mf])) (mf/defc resizing-textarea - {::mf/wrap-props false} - [props] - (let [value (obj/get props "value" "") - on-focus (obj/get props "on-focus") - on-blur (obj/get props "on-blur") - placeholder (obj/get props "placeholder") - on-change (obj/get props "on-change") - on-esc (obj/get props "on-esc") - autofocus? (obj/get props "autofocus") + {::mf/wrap-props false + ::mf/forward-ref true} + [props ref] + (let [value (d/nilv (unchecked-get props "value") "") + on-focus (unchecked-get props "on-focus") + on-blur (unchecked-get props "on-blur") + placeholder (unchecked-get props "placeholder") + on-change (unchecked-get props "on-change") + on-esc (unchecked-get props "on-esc") + autofocus? (unchecked-get props "autofocus") + select-on-focus? (unchecked-get props "select-on-focus") - ref (mf/use-ref) + local-ref (mf/use-ref) + ref (or ref local-ref) on-key-down - (mf/use-callback + (mf/use-fn (fn [event] (when (and (kbd/esc? event) (fn? on-esc)) (on-esc event)))) on-change* - (mf/use-callback + (mf/use-fn (mf/deps on-change) (fn [event] (let [content (dom/get-target-val event)] - (on-change content))))] + (on-change content)))) + + on-focus* + (mf/use-fn + (mf/deps select-on-focus? on-focus) + (fn [event] + (when (fn? on-focus) + (on-focus event)) + + (when ^boolean select-on-focus? + (let [target (dom/get-target event)] + (dom/select-text! target) + ;; In webkit browsers the mouseup event will be called after the on-focus causing and unselect + (.addEventListener target "mouseup" dom/prevent-default #js {:once true}))))) + ] + (mf/use-layout-effect @@ -64,7 +82,7 @@ {:ref ref :auto-focus autofocus? :on-key-down on-key-down - :on-focus on-focus + :on-focus on-focus* :on-blur on-blur :value value :placeholder placeholder @@ -76,24 +94,24 @@ content (mf/use-state "") on-focus - (mf/use-callback + (mf/use-fn #(reset! show-buttons? true)) on-blur - (mf/use-callback + (mf/use-fn #(reset! show-buttons? false)) on-change - (mf/use-callback + (mf/use-fn #(reset! content %)) on-cancel - (mf/use-callback + (mf/use-fn #(do (reset! content "") (reset! show-buttons? false))) on-submit - (mf/use-callback + (mf/use-fn (mf/deps thread @content) (fn [] (st/emit! (dcm/add-comment thread @content)) @@ -128,7 +146,7 @@ pos-y (* (:y position) zoom) on-esc - (mf/use-callback + (mf/use-fn (mf/deps draft) (fn [event] (dom/stop-propagation event) @@ -137,13 +155,13 @@ (st/emit! :interrupt)))) on-change - (mf/use-callback + (mf/use-fn (mf/deps draft) (fn [content] (st/emit! (dcm/update-draft-thread {:content content})))) on-submit - (mf/use-callback + (mf/use-fn (mf/deps draft) (partial on-submit draft))] @@ -179,16 +197,20 @@ (let [content (mf/use-state content) on-change - (mf/use-callback + (mf/use-fn #(reset! content %)) on-submit* - (mf/use-callback + (mf/use-fn (mf/deps @content) - (fn [] (on-submit @content)))] + (fn [] (on-submit @content))) + ] + [:div.reply-form.edit-form [:& resizing-textarea {:value @content + :autofocus true + :select-on-focus true :on-change on-change}] [:div.buttons [:input.btn-primary {:type "button" :value "Post" :on-click on-submit*}] @@ -202,24 +224,24 @@ edition? (mf/use-state false) on-show-options - (mf/use-callback #(reset! options true)) + (mf/use-fn #(reset! options true)) on-hide-options - (mf/use-callback #(reset! options false)) + (mf/use-fn #(reset! options false)) on-edit-clicked - (mf/use-callback + (mf/use-fn (fn [] (reset! options false) (reset! edition? true))) on-delete-comment - (mf/use-callback + (mf/use-fn (mf/deps comment) #(st/emit! (dcm/delete-comment comment))) delete-thread - (mf/use-callback + (mf/use-fn (mf/deps thread) #(st/emit! (dcm/close-thread) (if (= origin :viewer) @@ -228,7 +250,7 @@ on-delete-thread - (mf/use-callback + (mf/use-fn (mf/deps thread) #(st/emit! (modal/show {:type :confirm @@ -238,17 +260,17 @@ :on-accept delete-thread}))) on-submit - (mf/use-callback + (mf/use-fn (mf/deps comment thread) (fn [content] (reset! edition? false) (st/emit! (dcm/update-comment (assoc comment :content content))))) on-cancel - (mf/use-callback #(reset! edition? false)) + (mf/use-fn #(reset! edition? false)) toggle-resolved - (mf/use-callback + (mf/use-fn (mf/deps thread) (fn [event] (dom/stop-propagation event) @@ -268,6 +290,7 @@ (if (:is-resolved thread) [:span i/checkbox-checked] [:span i/checkbox-unchecked])]) + (when (= (:id profile) (:id owner)) [:div.options [:div.options-icon {:on-click on-show-options} i/actions]])] @@ -287,40 +310,45 @@ [:li {:on-click on-delete-thread} (tr "labels.delete-comment-thread")] [:li {:on-click on-delete-comment} (tr "labels.delete-comment")])]]])) -(defn comments-ref - [{:keys [id] :as thread}] - (l/derived (l/in [:comments id]) st/state)) +(defn make-comments-ref + [thread-id] + (l/derived (l/in [:comments thread-id]) st/state)) (mf/defc thread-comments {::mf/wrap [mf/memo]} [{:keys [thread zoom users origin position-modifier]}] - (let [ref (mf/use-ref) - pos (cond-> (:position thread) - (some? position-modifier) - (gpt/transform position-modifier)) + (let [ref (mf/use-ref) - pos-x (+ (* (:x pos) zoom) 14) - pos-y (- (* (:y pos) zoom) 14) - comments-ref (mf/use-memo (mf/deps thread) #(comments-ref thread)) + thread-id (:id thread) + thread-pos (:position thread) + + pos (cond-> thread-pos + (some? position-modifier) + (gpt/transform position-modifier)) + + pos-x (+ (* (:x pos) zoom) 14) + pos-y (- (* (:y pos) zoom) 14) + + comments-ref (mf/with-memo [thread-id] + (make-comments-ref thread-id)) comments-map (mf/deref comments-ref) - comments (->> (vals comments-map) - (sort-by :created-at)) + + comments (mf/with-memo [comments-map] + (->> (vals comments-map) + (sort-by :created-at))) + comment (first comments)] - (mf/use-layout-effect - (mf/deps thread) - #(st/emit! (dcm/retrieve-comments (:id thread)))) + (mf/with-effect [thread-id] + (st/emit! (dcm/retrieve-comments thread-id))) - (mf/use-effect - (mf/deps thread) - #(st/emit! (dcm/update-comment-thread-status thread))) + (mf/with-effect [thread-id] + (st/emit! (dcm/update-comment-thread-status thread-id))) - (mf/use-layout-effect - (mf/deps thread comments-map) - (fn [] - (when-let [node (mf/ref-val ref)] - (dom/scroll-into-view-if-needed! node)))) + (mf/with-layout-effect [thread-pos comments-map] + (when-let [node (mf/ref-val ref)] + (dom/scroll-into-view-if-needed! node))) (when (some? comment) [:div.thread-content @@ -345,22 +373,22 @@ (defn use-buble [zoom {:keys [position frame-id]}] (let [dragging-ref (mf/use-ref false) - start-ref (mf/use-ref nil) + start-ref (mf/use-ref nil) - state (mf/use-state {:hover false - :new-position-x nil - :new-position-y nil - :new-frame-id frame-id}) + state (mf/use-state {:hover false + :new-position-x nil + :new-position-y nil + :new-frame-id frame-id}) on-pointer-down - (mf/use-callback + (mf/use-fn (fn [event] (dom/capture-pointer event) (mf/set-ref-val! dragging-ref true) (mf/set-ref-val! start-ref (dom/get-client-position event)))) on-pointer-up - (mf/use-callback + (mf/use-fn (mf/deps (select-keys @state [:new-position-x :new-position-y :new-frame-id])) (fn [_ thread] (when (and @@ -369,7 +397,7 @@ (st/emit! (dwcm/update-comment-thread-position thread [(:new-position-x @state) (:new-position-y @state)]))))) on-lost-pointer-capture - (mf/use-callback + (mf/use-fn (fn [event] (dom/release-pointer event) (mf/set-ref-val! dragging-ref false) @@ -378,7 +406,7 @@ (swap! state assoc :new-position-y nil))) on-pointer-move - (mf/use-callback + (mf/use-fn (mf/deps position zoom) (fn [event] (when-let [_ (mf/ref-val dragging-ref)] @@ -416,7 +444,7 @@ pos-y (* (or (:new-position-y @state) (:y pos)) zoom) on-pointer-down* - (mf/use-callback + (mf/use-fn (mf/deps origin was-open? open? drag? on-pointer-down) (fn [event] (when (not= origin :viewer) @@ -427,7 +455,7 @@ (on-pointer-down event)))) on-pointer-up* - (mf/use-callback + (mf/use-fn (mf/deps origin thread was-open? drag? on-pointer-up) (fn [event] (when (not= origin :viewer) @@ -439,7 +467,7 @@ (st/emit! (dcm/open-thread thread)))))) on-pointer-move* - (mf/use-callback + (mf/use-fn (mf/deps origin drag? on-pointer-move) (fn [event] (when (not= origin :viewer) @@ -448,7 +476,7 @@ (on-pointer-move event)))) on-click* - (mf/use-callback + (mf/use-fn (mf/deps origin thread on-click) (fn [event] (dom/stop-propagation event) @@ -472,7 +500,7 @@ [{:keys [item users on-click]}] (let [owner (get users (:owner-id item)) on-click* - (mf/use-callback + (mf/use-fn (mf/deps item) (fn [event] (dom/stop-propagation event) diff --git a/frontend/src/app/main/ui/components/numeric_input.cljs b/frontend/src/app/main/ui/components/numeric_input.cljs index 93125b5e12..855ded34a6 100644 --- a/frontend/src/app/main/ui/components/numeric_input.cljs +++ b/frontend/src/app/main/ui/components/numeric_input.cljs @@ -200,11 +200,6 @@ (when (and (some? current) (not (.contains current target))) (dom/blur! current))))))) - on-mouse-up - (mf/use-callback - (fn [event] - (dom/prevent-default event))) - handle-focus (mf/use-callback (fn [event] @@ -213,9 +208,9 @@ (on-focus event)) (when select-on-focus? - (-> event (dom/get-target) (.select)) + (dom/select-text! event) ;; In webkit browsers the mouseup event will be called after the on-focus causing and unselect - (.addEventListener target "mouseup" on-mouse-up #js {"once" true}))))) + (.addEventListener target "mouseup" dom/prevent-default #js {:once true}))))) props (-> props (obj/without ["value" "onChange" "nillable" "onFocus"]) diff --git a/frontend/src/app/main/ui/dashboard/projects.cljs b/frontend/src/app/main/ui/dashboard/projects.cljs index eca9a2342e..d0f5f3f995 100644 --- a/frontend/src/app/main/ui/dashboard/projects.cljs +++ b/frontend/src/app/main/ui/dashboard/projects.cljs @@ -255,6 +255,7 @@ (fn [] (st/emit! (dd/fetch-files {:project-id project-id}) (dd/fetch-recent-files (:id team)) + (dd/fetch-projects) (dd/clear-selected-files))))] (mf/with-effect diff --git a/frontend/src/app/main/ui/shapes/fills.cljs b/frontend/src/app/main/ui/shapes/fills.cljs index 668ba8c399..de37f15609 100644 --- a/frontend/src/app/main/ui/shapes/fills.cljs +++ b/frontend/src/app/main/ui/shapes/fills.cljs @@ -16,6 +16,8 @@ [app.util.object :as obj] [rumext.v2 :as mf])) +(def no-repeat-padding 1.05) + (mf/defc fills {::mf/wrap-props false} [props] @@ -71,7 +73,10 @@ (let [fill-id (dm/str "fill-" shape-index "-" render-id)] [:> :pattern (-> (obj/clone pattern-attrs) - (obj/set! "id" fill-id)) + (obj/set! "id" fill-id) + (cond-> has-image? + (-> (obj/set! "width" (* width no-repeat-padding)) + (obj/set! "height" (* height no-repeat-padding))))) [:g (for [[fill-index value] (-> (d/enumerate (:fills shape [])) reverse)] [:> :rect (-> (attrs/extract-fill-attrs value render-id fill-index type) @@ -80,7 +85,17 @@ (obj/set! "height" height))]) (when has-image? - [:image {:href (or (:data-uri shape) (get embed uri uri)) - :preserveAspectRatio "none" - :width width - :height height}])]])]))))) + [:g + ;; We add this shape to add a padding so the patter won't repeat + ;; Issue: https://tree.taiga.io/project/penpot/issue/5583 + [:rect {:x 0 + :y 0 + :width (* width no-repeat-padding) + :height (* height no-repeat-padding) + :fill "none"}] + [:image {:href (or (:data-uri shape) (get embed uri uri)) + :preserveAspectRatio "none" + :x 0 + :y 0 + :width width + :height height}]])]])]))))) diff --git a/frontend/src/app/main/ui/workspace/colorpicker.cljs b/frontend/src/app/main/ui/workspace/colorpicker.cljs index b99302e36f..5b15d247da 100644 --- a/frontend/src/app/main/ui/workspace/colorpicker.cljs +++ b/frontend/src/app/main/ui/workspace/colorpicker.cljs @@ -68,7 +68,9 @@ (mf/deps current-color @drag?) (fn [color] (when (or (not= (str/lower (:hex color)) (str/lower (:hex current-color))) - (not= (:h color) (:h current-color))) + (not= (:h color) (:h current-color)) + (not= (:s color) (:s current-color)) + (not= (:v color) (:v current-color))) (let [recent-color (merge current-color color) recent-color (dc/materialize-color-components recent-color)] (st/emit! (dc/update-colorpicker-color recent-color (not @drag?))))))) diff --git a/frontend/src/app/main/ui/workspace/colorpicker/color_inputs.cljs b/frontend/src/app/main/ui/workspace/colorpicker/color_inputs.cljs index d9b409cf09..dad0b71e73 100644 --- a/frontend/src/app/main/ui/workspace/colorpicker/color_inputs.cljs +++ b/frontend/src/app/main/ui/workspace/colorpicker/color_inputs.cljs @@ -6,6 +6,7 @@ (ns app.main.ui.workspace.colorpicker.color-inputs (:require + [app.common.data :as d] [app.common.math :as mth] [app.util.color :as uc] [app.util.dom :as dom] @@ -17,6 +18,14 @@ val (str \# val))) +(defn value->hsv-value + [val] + (* 255 (/ val 100))) + +(defn hsv-value->value + [val] + (* (/ val 255) 100)) + (mf/defc color-inputs [{:keys [type color disable-opacity on-change]}] (let [{red :r green :g blue :b hue :h saturation :s value :v @@ -56,8 +65,11 @@ on-change-property (fn [property max-value] (fn [e] - (let [val (-> e dom/get-target-val (mth/clamp 0 max-value)) - val (if (#{:s} property) (/ val 100) val)] + (let [val (-> e dom/get-target-val d/parse-double (mth/clamp 0 max-value)) + val (case property + :s (/ val 100) + :v (value->hsv-value val) + val)] (when (not (nil? val)) (if (#{:r :g :b} property) (let [{:keys [r g b]} (merge color (hash-map property val)) @@ -89,10 +101,12 @@ property-ref (get refs ref-key)] (when (and property-val property-ref) (when-let [node (mf/ref-val property-ref)] - (case ref-key - (:s :alpha) (dom/set-value! node (* property-val 100)) - :hex (dom/set-value! node property-val) - (dom/set-value! node property-val)))))))) + (let [new-val + (case ref-key + (:s :alpha) (mth/precision (* property-val 100) 2) + :v (mth/precision (hsv-value->value property-val) 2) + property-val)] + (dom/set-value! node new-val)))))))) [:div.color-values {:class (when disable-opacity "disable-opacity")} @@ -149,9 +163,9 @@ :ref (:v refs) :type "number" :min 0 - :max 255 + :max 100 :default-value value - :on-change (on-change-property :v 255)}]]) + :on-change (on-change-property :v 100)}]]) (when (not disable-opacity) [:input.alpha-value {:id "alpha-value" diff --git a/frontend/src/app/main/ui/workspace/colorpicker/hsva.cljs b/frontend/src/app/main/ui/workspace/colorpicker/hsva.cljs index 0ea8b085c4..6933baeb3a 100644 --- a/frontend/src/app/main/ui/workspace/colorpicker/hsva.cljs +++ b/frontend/src/app/main/ui/workspace/colorpicker/hsva.cljs @@ -44,7 +44,7 @@ [:span.hsva-selector-label "V"] [:& slider-selector {:class "value" - :reverse? true + :reverse? false :max-value 255 :value value :on-change (handle-change-slider :v) diff --git a/frontend/src/app/main/ui/workspace/comments.cljs b/frontend/src/app/main/ui/workspace/comments.cljs index 02ac964b01..dff8689f20 100644 --- a/frontend/src/app/main/ui/workspace/comments.cljs +++ b/frontend/src/app/main/ui/workspace/comments.cljs @@ -29,12 +29,12 @@ [] (let [{cmode :mode cshow :show} (mf/deref refs/comments-local) update-mode - (mf/use-callback + (mf/use-fn (fn [mode] (st/emit! (dcm/update-filters {:mode mode})))) update-show - (mf/use-callback + (mf/use-fn (fn [mode] (st/emit! (dcm/update-filters {:show mode}))))] @@ -76,7 +76,7 @@ page-id (or page-id (mf/use-ctx ctx/current-page-id)) on-thread-click - (mf/use-callback + (mf/use-fn (mf/deps page-id) (fn [thread] (when (not= page-id (:page-id thread)) diff --git a/frontend/src/app/main/ui/workspace/left_toolbar.cljs b/frontend/src/app/main/ui/workspace/left_toolbar.cljs index cd4deb9dc7..cc2908eb53 100644 --- a/frontend/src/app/main/ui/workspace/left_toolbar.cljs +++ b/frontend/src/app/main/ui/workspace/left_toolbar.cljs @@ -81,7 +81,11 @@ (let [tool (-> (dom/get-current-target event) (dom/get-data "tool") (keyword))] - (st/emit! :interrupt (dw/select-for-drawing tool))))) + (st/emit! :interrupt + dw/clear-edition-mode) + + ;; Delay so anything that launched :interrupt can finish + (st/emit! 100 (dw/select-for-drawing tool))))) toggle-text-palette (mf/use-fn diff --git a/frontend/src/app/main/ui/workspace/viewport/frame_grid.cljs b/frontend/src/app/main/ui/workspace/viewport/frame_grid.cljs index c7b4032862..37a7265863 100644 --- a/frontend/src/app/main/ui/workspace/viewport/frame_grid.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/frame_grid.cljs @@ -7,7 +7,10 @@ (ns app.main.ui.workspace.viewport.frame-grid (:require [app.common.data :as d] + [app.common.data.macros :as dm] + [app.common.geom.shapes :as gsh] [app.common.math :as mth] + [app.common.pages.helpers :as cph] [app.common.types.shape-tree :as ctst] [app.common.uuid :as uuid] [app.main.refs :as refs] @@ -109,32 +112,70 @@ :strokeOpacity color-opacity :fill "none"}}]]))])) +(defn frame-clip-area + [{:keys [selrect]} parents] + (reduce + (fn [sr parent] + (cond-> sr + (and (not (cph/root? parent)) + (cph/frame-shape? parent) + (not (:show-content parent))) + (gsh/clip-selrect (:selrect parent)))) + selrect + parents)) + (mf/defc grid-display-frame - [{:keys [frame zoom]}] - (for [[index grid] (->> (:grids frame) - (filter :display) - (map-indexed vector))] - (let [props #js {:key (str (:id frame) "-grid-" index) - :frame frame - :zoom zoom - :grid grid}] - (case (:type grid) - :square [:> square-grid props] - :column [:> layout-grid props] - :row [:> layout-grid props])))) + {::mf/wrap [mf/memo]} + [{:keys [frame zoom transforming]}] + (let [frame-id (:id frame) + parents-ref (mf/with-memo [frame-id] (refs/shape-parents frame-id)) + parents (mf/deref parents-ref) + clip-area (frame-clip-area frame parents) + clip-id (dm/str (:id frame) "-grid-clip") + + transform? + (or (contains? transforming frame-id) + (some #(contains? transforming %) (map :id parents)))] + + (when-not transform? + [:g {:clip-path (dm/fmt "url(#%)" clip-id)} + [:defs + [:clipPath {:id clip-id} + [:rect {:x (:x clip-area) + :y (:y clip-area) + :width (:width clip-area) + :height (:height clip-area)}]]] + + (for [[index grid] (->> (:grids frame) + (filter :display) + (map-indexed vector))] + (let [props #js {:key (str (:id frame) "-grid-" index) + :frame frame + :zoom zoom + :grid grid}] + (case (:type grid) + :square [:> square-grid props] + :column [:> layout-grid props] + :row [:> layout-grid props])))]))) + +(defn has-grid? + [{:keys [grids]}] + (and (some? grids) + (d/not-empty? (->> grids (filter :display))))) (mf/defc frame-grid {::mf/wrap [mf/memo]} [{:keys [zoom transform selected focus]}] - (let [frames (mf/deref refs/workspace-frames) - transforming (when (some? transform) selected) - is-transform? #(contains? transforming (:id %))] + (let [frames (->> (mf/deref refs/workspace-frames) + (filter has-grid?)) + transforming (when (some? transform) selected)] [:g.grid-display {:style {:pointer-events "none"}} (for [frame frames] - (when (and (not (is-transform? frame)) + (when (and #_(not (is-transform? frame)) (not (ctst/rotated-frame? frame)) (or (empty? focus) (contains? focus (:id frame)))) [:& grid-display-frame {:key (str "grid-" (:id frame)) :zoom zoom - :frame frame}]))])) + :frame frame + :transforming transforming}]))])) diff --git a/frontend/src/app/util/color.cljs b/frontend/src/app/util/color.cljs index c4e4005686..428aa3fb56 100644 --- a/frontend/src/app/util/color.cljs +++ b/frontend/src/app/util/color.cljs @@ -193,4 +193,4 @@ (or (:color-library-name color) (:name color) (:color color) - (gradient-type->string (:type (:gradient color))))) \ No newline at end of file + (gradient-type->string (:type (:gradient color))))) diff --git a/frontend/src/app/util/dom.cljs b/frontend/src/app/util/dom.cljs index b07888181e..6bfe71ad56 100644 --- a/frontend/src/app/util/dom.cljs +++ b/frontend/src/app/util/dom.cljs @@ -234,7 +234,7 @@ (defn select-text! [^js node] - (when (and (some? node) (or (= "INPUT" (.-tagName node)) (= "TEXTAREA" (.-tagName node)))) + (when (some? node) (.select ^js node))) (defn ^boolean equals? diff --git a/frontend/src/app/util/geom/grid.cljs b/frontend/src/app/util/geom/grid.cljs index 29aece8017..dea9569ad8 100644 --- a/frontend/src/app/util/geom/grid.cljs +++ b/frontend/src/app/util/geom/grid.cljs @@ -25,28 +25,31 @@ (mth/floor (/ frame-length-no-margins (+ item-length gutter))))) (defn- calculate-generic-grid - [v width {:keys [size gutter margin item-length type]}] + [v total-length {:keys [size gutter margin item-length type]}] (let [size (if (number? size) size - (calculate-size width item-length margin gutter)) - parts (/ width size) + (calculate-size total-length item-length margin gutter)) - width' (min (or item-length ##Inf) (+ parts (- gutter) (/ gutter size) (- (/ (* margin 2) size)))) + parts (/ total-length size) + + item-length (if (number? item-length) + item-length + (+ parts (- gutter) (/ gutter size) (- (/ (* margin 2) size)))) offset (case type - :right (- width (* width' size) (* gutter (dec size)) margin) - :center (/ (- width (* width' size) (* gutter (dec size))) 2) + :right (- total-length (* item-length size) (* gutter (dec size)) margin) + :center (/ (- total-length (* item-length size) (* gutter (dec size))) 2) margin) gutter (if (= :stretch type) - (let [gutter (/ (- width (* width' size) (* margin 2)) (dec size))] + (let [gutter (max 0 gutter (/ (- total-length (* item-length size) (* margin 2)) (dec size)))] (if (d/num? gutter) gutter 0)) gutter) next-v (fn [cur-val] - (+ offset v (* (+ width' gutter) cur-val)))] + (+ offset v (* (+ item-length gutter) cur-val)))] - [size width' next-v gutter])) + [size item-length next-v gutter])) (defn- calculate-column-grid [{:keys [width height x y] :as frame} params]