diff --git a/CHANGES.md b/CHANGES.md index 97ed882213..fa618fa820 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -116,6 +116,7 @@ example. It's still usable as before, we just removed the example. - Fix switch variants with paths [Taiga #12841](https://tree.taiga.io/project/penpot/issue/12841) - Fix referencing typography tokens on font-family tokens [Taiga #12492](https://tree.taiga.io/project/penpot/issue/12492) - Fix horizontal scroll on layer panel [Taiga #12843](https://tree.taiga.io/project/penpot/issue/12843) +- Fix unicode handling on email template abbreviation filter [Github #7966](https://github.com/penpot/penpot/pull/7966) ## 2.11.1 diff --git a/backend/resources/app/email/invite-to-team/en.html b/backend/resources/app/email/invite-to-team/en.html index 5e61f1f675..337593902d 100644 --- a/backend/resources/app/email/invite-to-team/en.html +++ b/backend/resources/app/email/invite-to-team/en.html @@ -240,4 +240,4 @@ - \ No newline at end of file + diff --git a/backend/src/app/rpc/commands/auth.clj b/backend/src/app/rpc/commands/auth.clj index 742077e875..fb5db45ef7 100644 --- a/backend/src/app/rpc/commands/auth.clj +++ b/backend/src/app/rpc/commands/auth.clj @@ -307,7 +307,8 @@ :content-type (:mtype input)})] (:id sobject)) (catch Throwable cause - (l/err :hint "unable to import profile picture" + (l/wrn :hint "unable to import profile picture" + :uri uri :cause cause) nil))) diff --git a/backend/src/app/rpc/rlimit.clj b/backend/src/app/rpc/rlimit.clj index d181b843fe..03ae35831f 100644 --- a/backend/src/app/rpc/rlimit.clj +++ b/backend/src/app/rpc/rlimit.clj @@ -104,28 +104,29 @@ (def ^:private schema:limit [:and [:map - [::name :any] + [::name :keyword] [::strategy schema:strategy] [::key :string] - [::opts :string]] - [:or - [:map - [::capacity ::sm/int] - [::rate ::sm/int] - [::internal ::ct/duration] - [::params [::sm/vec :any]]] - [:map - [::nreq ::sm/int] - [::unit [:enum :days :hours :minutes :seconds :weeks]]]]]) + [::opts :string] + [::capacity {:optional true} ::sm/int] + [::rate {:optional true} ::sm/int] + [::interval {:optional true} ::ct/duration] + [::params {:optional true} [::sm/vec :any]] + [::permits {:optional true} ::sm/int] + [::unit {:optional true} [:enum :days :hours :minutes :seconds :weeks]]] + [:fn (fn [attrs] + (let [contains-fn (partial contains? attrs)] + (or (every? contains-fn [::capacity ::rate ::interval]) + (every? contains-fn [::permits ::unit]))))]]) (def ^:private schema:limits [:map-of :keyword [::sm/vec schema:limit]]) (def ^:private valid-limit-tuple? - (sm/lazy-validator schema:limit-tuple)) + (sm/validator schema:limit-tuple)) (def ^:private valid-rlimit-instance? - (sm/lazy-validator ::rpc/rlimit)) + (sm/validator ::rpc/rlimit)) (defmethod parse-limit :window [[name strategy opts :as vlimit]] @@ -134,16 +135,16 @@ (merge {::name name ::strategy strategy} - (if-let [[_ nreq unit] (re-find window-opts-re opts)] - (let [nreq (parse-long nreq)] - {::nreq nreq + (if-let [[_ permits unit] (re-find window-opts-re opts)] + (let [permits (parse-long permits)] + {::permits permits ::unit (case unit "d" :days "h" :hours "m" :minutes "s" :seconds "w" :weeks) - ::key (str "ratelimit.window." (d/name name)) + ::key (str "penpot.rlimit." (cf/get :tenant) ".window." (d/name name)) ::opts opts}) (ex/raise :type :validation :code :invalid-window-limit-opts @@ -164,15 +165,15 @@ ::interval interval ::opts opts ::params [(->seconds interval) rate capacity] - ::key (str "ratelimit.bucket." (d/name name))}) + ::key (str "penpot.rlimit." (cf/get :tenant) ".bucket." (d/name name))}) (ex/raise :type :validation :code :invalid-bucket-limit-opts :hint (str/ffmt "looks like '%' does not have a valid format" opts)))) (defmethod process-limit :bucket - [rconn user-id now {:keys [::key ::params ::service ::capacity ::interval ::rate] :as limit}] + [rconn profile-id now {:keys [::key ::params ::service ::capacity ::interval ::rate] :as limit}] (let [script (-> bucket-rate-limit-script - (assoc ::rscript/keys [(str key "." service "." user-id)]) + (assoc ::rscript/keys [(str key "." service "." profile-id)]) (assoc ::rscript/vals (conj params (->seconds now)))) result (rds/eval rconn script) allowed? (boolean (nth result 0)) @@ -192,18 +193,18 @@ (assoc ::lresult/remaining remaining)))) (defmethod process-limit :window - [rconn user-id now {:keys [::nreq ::unit ::key ::service] :as limit}] + [rconn profile-id now {:keys [::permits ::unit ::key ::service] :as limit}] (let [ts (ct/truncate now unit) ttl (ct/diff now (ct/plus ts {unit 1})) script (-> window-rate-limit-script - (assoc ::rscript/keys [(str key "." service "." user-id "." (ct/format-inst ts))]) - (assoc ::rscript/vals [nreq (->seconds ttl)])) + (assoc ::rscript/keys [(str key "." service "." profile-id "." (ct/format-inst ts))]) + (assoc ::rscript/vals [permits (->seconds ttl)])) result (rds/eval rconn script) allowed? (boolean (nth result 0)) remaining (nth result 1)] (l/trace :hint "limit processed" :service service - :limit (name (::name limit)) + :name (name (::name limit)) :strategy (name (::strategy limit)) :opts (::opts limit) :allowed allowed? @@ -214,8 +215,8 @@ (assoc ::lresult/reset (ct/plus ts {unit 1}))))) (defn- process-limits - [rconn user-id limits now] - (let [results (into [] (map (partial process-limit rconn user-id now)) limits) + [rconn profile-id limits now] + (let [results (into [] (map (partial process-limit rconn profile-id now)) limits) remaining (->> results (d/index-by ::name ::lresult/remaining) (uri/map->query-string)) @@ -227,7 +228,7 @@ (when rejected (l/warn :hint "rejected rate limit" - :user-id (str user-id) + :profile-id (str profile-id) :limit-service (-> rejected ::service name) :limit-name (-> rejected ::name name) :limit-strategy (-> rejected ::strategy name))) @@ -371,12 +372,9 @@ (defn- on-refresh-error [_ cause] (when-not (instance? java.util.concurrent.RejectedExecutionException cause) - (if-let [explain (-> cause ex-data ex/explain)] - (l/warn ::l/raw (str "unable to refresh config, invalid format:\n" explain) - ::l/sync? true) - (l/warn :hint "unexpected exception on loading config" - :cause cause - ::l/sync? true)))) + (l/warn :hint "unexpected exception on loading config" + :cause cause + ::l/sync? true))) (defn- get-config-path [] diff --git a/backend/src/app/rpc/rlimit/bucket.lua b/backend/src/app/rpc/rlimit/bucket.lua index 4200dec4d1..32512d3506 100644 --- a/backend/src/app/rpc/rlimit/bucket.lua +++ b/backend/src/app/rpc/rlimit/bucket.lua @@ -25,9 +25,9 @@ local allowed = filled >= requested local newTokens = filled if allowed then newTokens = filled - requested + redis.call("hset", tokensKey, "tokens", newTokens, "timestamp", timestamp) end -redis.call("hset", tokensKey, "tokens", newTokens, "timestamp", timestamp) redis.call("expire", tokensKey, ttl) return { allowed, newTokens } diff --git a/backend/src/app/util/template.clj b/backend/src/app/util/template.clj index 5c7a0b8c6e..2365423349 100644 --- a/backend/src/app/util/template.clj +++ b/backend/src/app/util/template.clj @@ -7,10 +7,18 @@ (ns app.util.template (:require [app.common.exceptions :as ex] + [cuerdas.core :as str] + [selmer.filters :as sf] [selmer.parser :as sp])) ;; (sp/cache-off!) +(sf/add-filter! :abbreviate + (fn [s n] + (let [n (parse-long n)] + (str/abbreviate s n)))) + + (defn render [path context] (try diff --git a/backend/src/app/worker/dispatcher.clj b/backend/src/app/worker/dispatcher.clj index f95ade8e1c..818c50c18c 100644 --- a/backend/src/app/worker/dispatcher.clj +++ b/backend/src/app/worker/dispatcher.clj @@ -137,33 +137,34 @@ RETURNING task.id, task.queue") ::wait))) (run-batch [] - (let [rconn (rds/connect cfg)] - (try - (-> cfg - (assoc ::rds/conn rconn) - (db/tx-run! run-batch')) + (try + (let [rconn (rds/connect cfg)] + (try + (-> cfg + (assoc ::rds/conn rconn) + (db/tx-run! run-batch')) + (finally + (.close ^AutoCloseable rconn)))) - (catch InterruptedException cause - (throw cause)) - (catch Exception cause - (cond - (rds/exception? cause) - (do - (l/wrn :hint "redis exception (will retry in an instant)" :cause cause) - (px/sleep timeout)) + (catch InterruptedException cause + (throw cause)) - (db/sql-exception? cause) - (do - (l/wrn :hint "database exception (will retry in an instant)" :cause cause) - (px/sleep timeout)) + (catch Exception cause + (cond + (rds/exception? cause) + (do + (l/wrn :hint "redis exception (will retry in an instant)" :cause cause) + (px/sleep timeout)) - :else - (do - (l/err :hint "unhandled exception (will retry in an instant)" :cause cause) - (px/sleep timeout)))) + (db/sql-exception? cause) + (do + (l/wrn :hint "database exception (will retry in an instant)" :cause cause) + (px/sleep timeout)) - (finally - (.close ^AutoCloseable rconn))))) + :else + (do + (l/err :hint "unhandled exception (will retry in an instant)" :cause cause) + (px/sleep timeout)))))) (dispatcher [] (l/inf :hint "started") @@ -176,7 +177,7 @@ RETURNING task.id, task.queue") (catch InterruptedException _ (l/trc :hint "interrupted")) (catch Throwable cause - (l/err :hint " unexpected exception" :cause cause)) + (l/err :hint "unexpected exception" :cause cause)) (finally (l/inf :hint "terminated"))))] diff --git a/common/deps.edn b/common/deps.edn index a2d9a1b1ec..e7e73645a5 100644 --- a/common/deps.edn +++ b/common/deps.edn @@ -30,7 +30,7 @@ integrant/integrant {:mvn/version "1.0.0"} funcool/tubax {:mvn/version "2021.05.20-0"} - funcool/cuerdas {:mvn/version "2025.06.16-414"} + funcool/cuerdas {:mvn/version "2026.415"} funcool/promesa {:git/sha "46048fc0d4bf5466a2a4121f5d52aefa6337f2e8" :git/url "https://github.com/funcool/promesa"} diff --git a/common/src/app/common/types/tokens_lib.cljc b/common/src/app/common/types/tokens_lib.cljc index 930b1f8e05..55e4d0cf41 100644 --- a/common/src/app/common/types/tokens_lib.cljc +++ b/common/src/app/common/types/tokens_lib.cljc @@ -1410,8 +1410,8 @@ Will return a value that matches this schema: ;; NOTE: we can't assign statically at eval time the value of a ;; function that is declared but not defined; so we need to pass ;; an anonymous function and delegate the resolution to runtime - {:encode/json #(export-dtcg-json %) - :decode/json #(read-multi-set-dtcg %) + {:encode/json #(some-> % export-dtcg-json) + :decode/json #(some-> % read-multi-set-dtcg) ;; FIXME: add better, more reallistic generator :gen/gen (->> (sg/small-int) (sg/fmap (fn [_] diff --git a/frontend/src/app/main/data/workspace/colors.cljs b/frontend/src/app/main/data/workspace/colors.cljs index 38cfe651ac..5856cec029 100644 --- a/frontend/src/app/main/data/workspace/colors.cljs +++ b/frontend/src/app/main/data/workspace/colors.cljs @@ -14,7 +14,7 @@ [app.common.types.fills :as types.fills] [app.common.types.library :as ctl] [app.common.types.shape :as shp] - [app.common.types.shape.shadow :refer [check-shadow]] + [app.common.types.shape.shadow :as types.shadow] [app.common.types.text :as txt] [app.main.broadcast :as mbc] [app.main.data.helpers :as dsh] @@ -406,30 +406,30 @@ (defn change-shadow [ids attrs index] - (ptk/reify ::change-shadow - ptk/WatchEvent - (watch [_ _ _] - (rx/of (dwsh/update-shapes - ids - (fn [shape] - (let [;; If we try to set a gradient to a shadow (for - ;; example using the color selection from - ;; multiple shapes) let's use the first stop - ;; color - attrs (cond-> attrs - (:gradient attrs) - (dm/get-in [:gradient :stops 0])) + (letfn [(update-shadow [shape] + (let [;; If we try to set a gradient to a shadow (for + ;; example using the color selection from + ;; multiple shapes) let's use the first stop + ;; color + attrs (cond-> attrs + (:gradient attrs) + (-> (dm/get-in [:gradient :stops 0]) + (select-keys types.shadow/color-attrs))) - attrs' (-> (dm/get-in shape [:shadow index :color]) - (merge attrs) - (d/without-nils))] - (assoc-in shape [:shadow index :color] attrs')))))))) + attrs' (-> (dm/get-in shape [:shadow index :color]) + (merge attrs) + (d/without-nils))] + (assoc-in shape [:shadow index :color] attrs')))] + (ptk/reify ::change-shadow + ptk/WatchEvent + (watch [_ _ _] + (rx/of (dwsh/update-shapes ids update-shadow)))))) (defn add-shadow [ids shadow] (assert - (check-shadow shadow) + (types.shadow/check-shadow shadow) "expected a valid shadow struct") (assert @@ -1146,16 +1146,16 @@ (defn- shadow->color-attr "Given a stroke map enriched with :shape-id, :index, and optionally :has-token-applied / :token-name, returns a color attribute map. - + If :has-token-applied is true, adds token metadata to :attrs: {:has-token-applied true :token-name } - + Args: - stroke: map with stroke info, including :shape-id and :index - file-id: current file UUID - libraries: map of shared color libraries - + Returns: A map like: {:attrs {...color data...} @@ -1260,12 +1260,12 @@ will include extra attributes in its :attrs map: {:has-token-applied true :token-name } - + Args: - shapes: vector of shape maps - file-id: current file UUID - libraries: map of shared color libraries - + Returns: A vector of color attribute maps with metadata for each shape." [shapes file-id libraries] diff --git a/render-wasm/src/shapes/modifiers.rs b/render-wasm/src/shapes/modifiers.rs index 02e7911aaa..f8e0c6428a 100644 --- a/render-wasm/src/shapes/modifiers.rs +++ b/render-wasm/src/shapes/modifiers.rs @@ -277,7 +277,6 @@ fn propagate_reflow( }; let shapes = &state.shapes; - let mut reflow_parent = false; if reflown.contains(id) { return; @@ -294,15 +293,10 @@ fn propagate_reflow( // If this is a fill layout but the parent has not been reflown yet // we wait for the next iteration for reflow skip_reflow = true; - reflow_parent = true; } } } - if shape.is_layout_vertical_auto() || shape.is_layout_horizontal_auto() { - reflow_parent = true; - } - if !skip_reflow { layout_reflows.push(*id); } @@ -312,32 +306,26 @@ fn propagate_reflow( if let Some(child) = shapes.get(&children_ids[0]) { let child_bounds = bounds.find(child); bounds.insert(shape.id, child_bounds); - reflow_parent = true; } reflown.insert(*id); } Type::Group(_) => { if let Some(shape_bounds) = calculate_group_bounds(shape, shapes, bounds) { bounds.insert(shape.id, shape_bounds); - reflow_parent = true; } reflown.insert(*id); } Type::Bool(_) => { if let Some(shape_bounds) = calculate_bool_bounds(shape, shapes, bounds, modifiers) { bounds.insert(shape.id, shape_bounds); - reflow_parent = true; } reflown.insert(*id); } - _ => { - // Other shapes don't have to be reflown - reflow_parent = true; - } + _ => {} } if let Some(parent) = shape.parent_id.and_then(|id| shapes.get(&id)) { - if reflow_parent && (parent.has_layout() || parent.is_group_like()) { + if parent.has_layout() || parent.is_group_like() { entries.push_back(Modifier::reflow(parent.id)); } } diff --git a/render-wasm/src/shapes/modifiers/flex_layout.rs b/render-wasm/src/shapes/modifiers/flex_layout.rs index 7246d167e6..3a7c9929f2 100644 --- a/render-wasm/src/shapes/modifiers/flex_layout.rs +++ b/render-wasm/src/shapes/modifiers/flex_layout.rs @@ -344,6 +344,7 @@ fn distribute_fill_across_space(layout_axis: &LayoutAxis, tracks: &mut [TrackDat let mut size = track.across_size - child.margin_across_start - child.margin_across_end; size = size.clamp(child.min_across_size, child.max_across_size); + size = f32::min(size, layout_axis.across_space()); child.across_size = size; } } @@ -545,14 +546,22 @@ fn child_position( align_self: Some(align_self), .. }) => match align_self { - AlignSelf::Center => (track.across_size - child_axis.across_size) / 2.0, + AlignSelf::Center => { + (track.across_size - child_axis.across_size + child_axis.margin_across_start + - child_axis.margin_across_end) + / 2.0 + } AlignSelf::End => { track.across_size - child_axis.across_size - child_axis.margin_across_end } _ => child_axis.margin_across_start, }, _ => match layout_data.align_items { - AlignItems::Center => (track.across_size - child_axis.across_size) / 2.0, + AlignItems::Center => { + (track.across_size - child_axis.across_size + child_axis.margin_across_start + - child_axis.margin_across_end) + / 2.0 + } AlignItems::End => { track.across_size - child_axis.across_size - child_axis.margin_across_end } @@ -578,7 +587,11 @@ pub fn reflow_flex_layout( let tracks = calculate_track_data(shape, layout_data, flex_data, layout_bounds, shapes, bounds); for track in tracks.iter() { - let total_shapes_size = track.shapes.iter().map(|s| s.main_size).sum::(); + let total_shapes_size = track + .shapes + .iter() + .map(|s| s.main_size + s.margin_main_start + s.margin_main_end) + .sum::(); let mut shape_anchor = first_anchor(layout_data, &layout_axis, track, total_shapes_size); for child_axis in track.shapes.iter() {