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

This commit is contained in:
Andrey Antukh 2026-06-23 12:26:17 +02:00
commit 9259b596dc
10 changed files with 274 additions and 26 deletions

View File

@ -487,7 +487,13 @@
:fn (mg/resource "app/migrations/sql/0149-mod-file-library-rel-synced-at.sql")}
{:name "0150-mod-http-session-v2"
:fn (mg/resource "app/migrations/sql/0150-mod-http-session-v2.sql")}])
:fn (mg/resource "app/migrations/sql/0150-mod-http-session-v2.sql")}
{:name "0150-mod-storage-object-table"
:fn (mg/resource "app/migrations/sql/0150-mod-storage-object-table.sql")}
{:name "0151-mod-file-tagged-object-thumbnail-table"
:fn (mg/resource "app/migrations/sql/0151-mod-file-tagged-object-thumbnail-table.sql")}])
(defn apply-migrations!
[pool name migrations]

View File

@ -0,0 +1 @@
CREATE INDEX IF NOT EXISTS storage_object__metadata_upload_id__idx ON storage_object ((metadata->>'~:upload-id')) WHERE deleted_at IS NULL;

View File

@ -0,0 +1 @@
CREATE INDEX IF NOT EXISTS file_tagged_object_thumbnail__object_id__idx ON file_tagged_object_thumbnail (object_id);

View File

@ -9,7 +9,7 @@
data resources."
(:refer-clojure :exclude [read-string hash-map merge name update-vals
parse-double group-by iteration concat mapcat
parse-uuid max min regexp? array?])
parse-uuid max min regexp? array? empty?])
#?(:cljs
(:require-macros [app.common.data]))
@ -175,11 +175,17 @@
(.isArray (class o))
false)))
(defn empty?
[val]
(if (or (coll? val) (string? val))
(clojure.core/empty? val)
(nil? val)))
(defn not-empty?
[coll]
(if (coll? coll)
(boolean (seq coll))
(not (nil? coll))))
[val]
(if (or (coll? val) (string? val))
(boolean (seq val))
(some? val)))
(defn editable-collection?
[m]

View File

@ -6,12 +6,14 @@
(ns app.common.test-helpers.tokens
(:require
[app.common.data :as d]
[app.common.test-helpers.files :as thf]
[app.common.test-helpers.shapes :as ths]
[app.common.types.container :as ctn]
[app.common.types.file :as ctf]
[app.common.types.pages-list :as ctpl]
[app.common.types.shape-tree :as ctst]
[app.common.types.text :as ctt]
[app.common.types.token :as cto]
[app.common.types.tokens-lib :as ctob]))
@ -81,11 +83,16 @@
:token {:name token-name}
:attributes token-attrs})
(reduce (fn [shape attr]
(case attr
:stroke-width (set-stroke-width shape resolved-value)
:stroke-color (set-stroke-color shape resolved-value)
:fill (set-fill-color shape resolved-value)
(ctn/set-shape-attr shape attr resolved-value {:ignore-touched true})))
(if (ctt/text-node-attr? attr)
(let [value (if (sequential? resolved-value) (first resolved-value) resolved-value)]
(ctt/update-text-content shape
ctt/is-content-node?
d/txt-merge {attr value}))
(case attr
:stroke-width (set-stroke-width shape resolved-value)
:stroke-color (set-stroke-color shape resolved-value)
:fill (set-fill-color shape resolved-value)
(ctn/set-shape-attr shape attr resolved-value {:ignore-touched true}))))
$
shape-attrs)))]

View File

@ -78,6 +78,10 @@
text-transform-attrs
text-fills))
(defn text-node-attr?
[attr]
(d/index-of text-node-attrs attr))
(def text-all-attrs (d/concat-set shape-attrs root-attrs paragraph-attrs text-node-attrs))
(def text-style-attrs
@ -201,6 +205,27 @@
[text]
(subs text 0 (min 280 (count text))))
(defn- compare-text-attr
"Compare two attribute values and return true if they are different.
Take into account the following:
- Only process keys that belong to text node attrs (ignore deprecated
attributes or other things that may be attached).
- Consider nil values, empty strings or empty lists all equal.
- Normalize numeric values (legacy) into strings.
- No value is equal than the default value."
[key value1 value2]
(when (text-node-attr? key)
(let [default-value (get default-text-attrs key)
normalize-value (fn [value]
(as-> value $
(if (number? $) (str $) $)
(if (or (d/empty? $) (= $ default-value))
nil
$)))
value1' (normalize-value value1)
value2' (normalize-value value2)]
(not= value1' value2'))))
(defn- compare-text-content
"Given two content text structures, conformed by maps and vectors,
compare them, and returns a set with the differences info.
@ -245,8 +270,7 @@
;; If the key is not :text, and they are different, it is an attribute difference.
;; Take into account that some processes remove empty attributes, so in some
;; cases we will compare [] with nil, and this is not a difference.
(if (and (not= v1 v2)
(or (d/not-empty? v1) (d/not-empty? v2)))
(if (compare-text-attr k v1 v2)
(attribute-cb acc k)
acc))))
#{}

View File

@ -111,8 +111,8 @@
(tht/apply-token-to-shape :frame1 "token-color" [:stroke-color] [:stroke-color] "#00ff00")
(tht/apply-token-to-shape :frame1 "token-color" [:fill] [:fill] "#00ff00")
(tht/apply-token-to-shape :frame1 "token-dimensions" [:width :height] [:width :height] 100)
(tht/apply-token-to-shape :text1 "token-font-size" [:font-size] [:font-size] 24)
(tht/apply-token-to-shape :text1 "token-letter-spacing" [:letter-spacing] [:letter-spacing] 2)
(tht/apply-token-to-shape :text1 "token-font-size" [:font-size] [:font-size] "24")
(tht/apply-token-to-shape :text1 "token-letter-spacing" [:letter-spacing] [:letter-spacing] "2")
(tht/apply-token-to-shape :text1 "token-font-family" [:font-family] [:font-family] ["Helvetica" "Arial" "sans-serif"])
(tht/apply-token-to-shape :circle1
"token-sizing"
@ -331,7 +331,7 @@
d/txt-merge
{:fills (ths/sample-fills-color :fill-color "#fabada")
:font-size "1"
:letter-spacing "0"
:letter-spacing "3"
:font-family "Arial"}))
(:objects page)
{})
@ -360,3 +360,40 @@
(t/is (= (count applied-tokens-frame') 0))
(t/is (= (count applied-tokens-text') 0))
(t/is (= (count applied-tokens-circle') 0))))
(t/deftest dont-unapply-automatic-when-null-change
(let [;; ==== Setup
file (-> (setup-file)
(tht/apply-token-to-shape :text1 "token-font-size" [:font-size] [:font-size] "14")
(tht/apply-token-to-shape :text1 "token-letter-spacing" [:letter-spacing] [:letter-spacing] "2")
(tht/apply-token-to-shape :text1 "token-font-family" [:font-family] [:font-family] ["Helvetica" "Arial" "sans-serif"]))
page (thf/current-page file)
text1 (ths/get-shape file :text1)
;; ==== Action
changes (-> (-> (pcb/empty-changes nil)
(pcb/with-page page)
(pcb/with-objects (:objects page)))
;; Some changes in text content are not semantic changes
;; and thus they don't unapply tokens (e.g. adding an attribute
;; with value nil or with the default value).
(cls/generate-update-shapes [(:id text1)]
(fn [shape]
(txt/update-text-content
shape
txt/is-content-node?
d/txt-merge
{:font-size nil
:line-height "3"
:nonexistent-attr "sample value"}))
(:objects page)
{}))
file' (thf/apply-changes file changes)
;; ==== Get
text1' (ths/get-shape file' :text1)
applied-tokens-text' (:applied-tokens text1')]
;; ==== Check
(t/is (= (count applied-tokens-text') 3))))

View File

@ -16,6 +16,8 @@
(get :content)
(cttx/change-text "hello world")))
;; Normal happy-path changes
(def content-changed-text
(assoc-in content-base [:children 0 :children 0 :children 0 :text] "changed"))
@ -34,22 +36,127 @@
(update-in content-base [:children 0 :children 0 :children]
#(conj % (assoc line :font-weight "700"))))
(def content-changed-structure-same-attrs
(update-in content-base [:children 0 :children 0 :children] #(conj % line)))
;; Special cases
;; Numeric value (legacy): number 14 should normalize to string "14",
;; which equals the default value in content-base
(def content-numeric-font-size-default
(assoc-in content-base [:children 0 :children 0 :children 0 :font-size] 14))
;; Numeric value (legacy): number 32 should normalize to string "32",
;; matching content-changed-attr (which uses string "32")
(def content-numeric-font-size-32
(assoc-in content-base [:children 0 :children 0 :children 0 :font-size] 32))
;; Attribute set to nil (removed)
(def content-nil-font-size
(assoc-in content-base [:children 0 :children 0 :children 0 :font-size] nil))
;; Attribute set to empty string
(def content-empty-string-font-size
(assoc-in content-base [:children 0 :children 0 :children 0 :font-size] ""))
;; Attribute set to empty vector
(def content-empty-list-font-size
(assoc-in content-base [:children 0 :children 0 :children 0 :font-size] []))
;; Attribute set to its default value (font-size default is "14")
(def content-default-font-size
(assoc-in content-base [:children 0 :children 0 :children 0 :font-size] "14"))
;; Non-text-node-attr key change (grow-type is not in text-node-attrs)
(def content-non-text-node-attr
(assoc-in content-base [:children 0 :children 0 :children 0 :grow-type] "auto-height"))
;; Other text-node-attr categories
(def content-changed-font-family
(assoc-in content-base [:children 0 :children 0 :children 0 :font-family] "Arial"))
(def content-changed-line-height
(assoc-in content-base [:children 0 :children 0 :children 0 :line-height] "1.5"))
(def content-changed-letter-spacing
(assoc-in content-base [:children 0 :children 0 :children 0 :letter-spacing] "2"))
(def content-changed-text-decoration
(assoc-in content-base [:children 0 :children 0 :children 0 :text-decoration] "underline"))
(def content-changed-text-transform
(assoc-in content-base [:children 0 :children 0 :children 0 :text-transform] "uppercase"))
(def content-changed-typography-ref-id
(assoc-in content-base [:children 0 :children 0 :children 0 :typography-ref-id] "new-typography-id"))
(def content-changed-fills
(assoc-in content-base [:children 0 :children 0 :children 0 :fills]
[{:fill-color "#ff0000" :fill-opacity 1}]))
(t/deftest test-get-diff-type
(let [diff-text (cttx/get-diff-type content-base content-changed-text)
diff-attr (cttx/get-diff-type content-base content-changed-attr)
diff-both (cttx/get-diff-type content-base content-changed-both)
diff-structure (cttx/get-diff-type content-base content-changed-structure)
diff-structure-same-attrs (cttx/get-diff-type content-base content-changed-structure-same-attrs)]
diff-structure-same-attrs (cttx/get-diff-type content-base content-changed-structure-same-attrs)
;; Numeric normalization: number 14 → string "14" → default → nil (same as base)
diff-numeric-default (cttx/get-diff-type content-base content-numeric-font-size-default)
;; Numeric normalization: number 32 → string "32" (different from base's "14")
diff-numeric-different (cttx/get-diff-type content-base content-numeric-font-size-32)
;; Numeric 32 vs string "32": should be equal after normalization
diff-numeric-vs-string (cttx/get-diff-type content-changed-attr content-numeric-font-size-32)
;; nil / empty-string / empty-list / default → all normalize to nil
diff-nil-attr (cttx/get-diff-type content-base content-nil-font-size)
diff-empty-string (cttx/get-diff-type content-base content-empty-string-font-size)
diff-empty-list (cttx/get-diff-type content-base content-empty-list-font-size)
diff-default-value (cttx/get-diff-type content-base content-default-font-size)
;; Non-text-node-attr key: should be ignored
diff-non-text-node (cttx/get-diff-type content-base content-non-text-node-attr)
;; Other text-node-attr categories
diff-font-family (cttx/get-diff-type content-base content-changed-font-family)
diff-line-height (cttx/get-diff-type content-base content-changed-line-height)
diff-letter-spacing (cttx/get-diff-type content-base content-changed-letter-spacing)
diff-text-decoration (cttx/get-diff-type content-base content-changed-text-decoration)
diff-text-transform (cttx/get-diff-type content-base content-changed-text-transform)
diff-typography-ref (cttx/get-diff-type content-base content-changed-typography-ref-id)
diff-fills (cttx/get-diff-type content-base content-changed-fills)]
;; Basic cases
(t/is (= #{:text-content-text} diff-text))
(t/is (= #{:text-content-attribute} diff-attr))
(t/is (= #{:text-content-text :text-content-attribute} diff-both))
(t/is (= #{:text-content-structure} diff-structure))
(t/is (= #{:text-content-structure} diff-structure-same-attrs))))
(t/is (= #{:text-content-structure} diff-structure-same-attrs))
;; Numeric normalization
(t/is (= #{} diff-numeric-default))
(t/is (= #{:text-content-attribute} diff-numeric-different))
(t/is (= #{} diff-numeric-vs-string))
;; nil / empty / default normalization (content-base has default font-size "14",
;; which normalizes to nil; nil also normalizes to nil → equal)
(t/is (= #{} diff-nil-attr))
(t/is (= #{} diff-empty-string))
(t/is (= #{} diff-empty-list))
(t/is (= #{} diff-default-value))
;; Non-text-node-attr key is ignored
(t/is (= #{} diff-non-text-node))
;; Each text-node-attr category triggers attribute diff
(t/is (= #{:text-content-attribute} diff-font-family))
(t/is (= #{:text-content-attribute} diff-line-height))
(t/is (= #{:text-content-attribute} diff-letter-spacing))
(t/is (= #{:text-content-attribute} diff-text-decoration))
(t/is (= #{:text-content-attribute} diff-text-transform))
(t/is (= #{:text-content-attribute} diff-typography-ref))
(t/is (= #{:text-content-attribute} diff-fills))))
(t/deftest test-get-diff-attrs
@ -57,13 +164,62 @@
attrs-attr (cttx/get-diff-attrs content-base content-changed-attr)
attrs-both (cttx/get-diff-attrs content-base content-changed-both)
attrs-structure (cttx/get-diff-attrs content-base content-changed-structure)
attrs-structure-same-attrs (cttx/get-diff-attrs content-base content-changed-structure-same-attrs)]
attrs-structure-same-attrs (cttx/get-diff-attrs content-base content-changed-structure-same-attrs)
;; Numeric normalization: number 14 → string "14" → default → nil (same as base)
attrs-numeric-default (cttx/get-diff-attrs content-base content-numeric-font-size-default)
;; Numeric normalization: number 32 → string "32" (different from base's "14")
attrs-numeric-different (cttx/get-diff-attrs content-base content-numeric-font-size-32)
;; Numeric 32 vs string "32": should be equal after normalization
attrs-numeric-vs-string (cttx/get-diff-attrs content-changed-attr content-numeric-font-size-32)
;; nil / empty-string / empty-list / default → all normalize to nil
attrs-nil-attr (cttx/get-diff-attrs content-base content-nil-font-size)
attrs-empty-string (cttx/get-diff-attrs content-base content-empty-string-font-size)
attrs-empty-list (cttx/get-diff-attrs content-base content-empty-list-font-size)
attrs-default-value (cttx/get-diff-attrs content-base content-default-font-size)
;; Non-text-node-attr key: should be ignored
attrs-non-text-node (cttx/get-diff-attrs content-base content-non-text-node-attr)
;; Other text-node-attr categories
attrs-font-family (cttx/get-diff-attrs content-base content-changed-font-family)
attrs-line-height (cttx/get-diff-attrs content-base content-changed-line-height)
attrs-letter-spacing (cttx/get-diff-attrs content-base content-changed-letter-spacing)
attrs-text-decoration (cttx/get-diff-attrs content-base content-changed-text-decoration)
attrs-text-transform (cttx/get-diff-attrs content-base content-changed-text-transform)
attrs-typography-ref (cttx/get-diff-attrs content-base content-changed-typography-ref-id)
attrs-fills (cttx/get-diff-attrs content-base content-changed-fills)]
;; Basic cases
(t/is (= #{} attrs-text))
(t/is (= #{:font-size} attrs-attr))
(t/is (= #{:font-size} attrs-both))
(t/is (= #{} attrs-structure))
(t/is (= #{} attrs-structure-same-attrs))))
(t/is (= #{} attrs-structure-same-attrs))
;; Numeric normalization
(t/is (= #{} attrs-numeric-default))
(t/is (= #{:font-size} attrs-numeric-different))
(t/is (= #{} attrs-numeric-vs-string))
;; nil / empty / default normalization
(t/is (= #{} attrs-nil-attr))
(t/is (= #{} attrs-empty-string))
(t/is (= #{} attrs-empty-list))
(t/is (= #{} attrs-default-value))
;; Non-text-node-attr key is ignored
(t/is (= #{} attrs-non-text-node))
;; Each text-node-attr category reports correct attr key
(t/is (= #{:font-family} attrs-font-family))
(t/is (= #{:line-height} attrs-line-height))
(t/is (= #{:letter-spacing} attrs-letter-spacing))
(t/is (= #{:text-decoration} attrs-text-decoration))
(t/is (= #{:text-transform} attrs-text-transform))
(t/is (= #{:typography-ref-id} attrs-typography-ref))
(t/is (= #{:fills} attrs-fills))))
(t/deftest test-equal-structure

View File

@ -399,6 +399,7 @@
(fn [_]
;; Start the event collection pipeline
(->> (rx/merge
(rx/filter (ptk/type? ::force-persist) stream)
(->> (rx/from-atom buffer)
(rx/filter #(pos? (count %)))
(rx/debounce 2000))
@ -416,6 +417,7 @@
(rx/map (constantly chunk))))))
(rx/take-until stopper)
(rx/subs! (fn [chunk]
(st/emit! (ptk/data-event ::chunk-persisted {:chunk chunk}))
(swap! buffer remove-from-buffer (count chunk)))
(fn [cause]
(l/error :hint "unexpected error on audit persistence" :cause cause))

View File

@ -45,6 +45,7 @@
[app.util.i18n :as i18n :refer [tr]]
[app.util.keyboard :as kbd]
[beicon.v2.core :as rx]
[potok.v2.core :as ptk]
[rumext.v2 :as mf]))
(mf/defc shortcuts*
@ -958,17 +959,24 @@
ev-name (if (= next-renderer :wasm)
"enable-webgl-rendering"
"disable-webgl-rendering")]
(if (cf/external-feature-flag "renderer-hard-reload" "test")
;; Bare RPC + hard reload: skips `du/update-profile-props`, so
;; `features/recompute-features` is not run here; bootstrap
;; after reload resolves render-wasm/v1 from the saved profile.
(do
(st/emit! (ev/event {::ev/name ev-name
::ev/origin "workspace:menu"}))
(->> (rp/cmd! :update-profile-props {:props {:renderer next-renderer}})
(rx/subs! (fn [_] (dom/reload-current-window true))
(->> (rx/zip
(rp/cmd! :update-profile-props {:props {:renderer next-renderer}})
(rx/filter (ptk/type? ::ev/chunk-persisted) st/stream))
(rx/timeout 2000 (rx/of :timeout))
(rx/subs! (fn [_]
(dom/reload-current-window true))
(fn [_]
(st/emit! (ntf/error (tr "errors.generic")))))))
(st/emit! (ntf/error (tr "errors.generic"))))))
(st/emit! (ev/event {::ev/name ev-name
::ev/origin "workspace:menu"})
(ptk/data-event ::ev/force-persist {})))
;; `update-profile-props` WatchEvent calls
;; `features/recompute-features`.
(st/emit! (ev/event {::ev/name ev-name