Compare commits

...
Author SHA1 Message Date
Andrés Moya ccf7891479 wip 2026-09-08 12:20:03 +02:00
Andrés Moya deb0583029 🔧 Add tests and validations to variant functions 2026-09-08 11:54:38 +02:00
Andrés Moya a5b19118b9 💄 Rename props -> properties 2026-09-08 11:54:38 +02:00
Andrés Moya 0f66caf542 🔧 Validate changes immediately after processing them 2026-09-08 11:54:38 +02:00
15 changed files with 672 additions and 181 deletions

No files matched your search

+87 -44
View File
@@ -6,6 +6,7 @@
(ns app.common.files.changes
(:require
#?(:cljs [app.common.files.validate :as val])
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.exceptions :as ex]
@@ -428,7 +429,14 @@
[:set-base-font-size
[:map {:title "ModBaseFontSize"}
[:type [:= :set-base-font-size]]
[:base-font-size :string]]]])
[:base-font-size :string]]]
[:validate-shapes
[:map {:title "ValidateShapesChange"}
[:type [:= :validate-shapes]]
[:page-id ::sm/uuid]
[:shape-ids [:vector ::sm/uuid]]
[:context :string]]]])
(def schema:changes
[:sequential {:gen/max 5 :gen/min 1} schema:change])
@@ -464,7 +472,7 @@
to the processor backend."
nil)
(defmulti process-change (fn [_ change] (:type change)))
(defmulti process-change (fn [_ change _] (:type change)))
(defmulti process-operation (fn [_ op] (:type op)))
;; Changes Processing Impl
@@ -496,22 +504,25 @@
(defn process-changes
([data items]
(process-changes data items true))
(process-changes data items true {}))
([data items verify?]
(process-changes data items verify? {}))
([data items verify? libraries]
;; When verify? false we spec the schema validation. Currently used
;; to make just 1 validation even if the changes are applied twice
(when verify?
(check-changes items))
(binding [*touched-changes* (volatile! #{})]
(let [result (reduce #(or (process-change %1 %2) %1) data items)]
(let [result (reduce #(or (process-change %1 %2 libraries) %1) data items)]
(reduce process-touched-change result @*touched-changes*)))))
;; --- Comment Threads
(defmethod process-change :set-comment-thread-position
[data {:keys [page-id comment-thread-id position frame-id]}]
[data {:keys [page-id comment-thread-id position frame-id]} _]
(d/update-in-when data [:pages-index page-id]
(fn [page]
(if (and position frame-id)
@@ -524,7 +535,7 @@
;; --- Guides
(defmethod process-change :set-guide
[data {:keys [page-id id params]}]
[data {:keys [page-id id params]} _]
(if (nil? params)
(d/update-in-when data [:pages-index page-id]
(fn [page]
@@ -540,7 +551,7 @@
;; --- Flows
(defmethod process-change :set-flow
[data {:keys [page-id id params]}]
[data {:keys [page-id id params]} _]
(if (nil? params)
(d/update-in-when data [:pages-index page-id]
(fn [page]
@@ -556,7 +567,7 @@
;; --- Grids
(defmethod process-change :set-default-grid
[data {:keys [page-id grid-type params]}]
[data {:keys [page-id grid-type params]} _]
(if (nil? params)
(d/update-in-when data [:pages-index page-id]
(fn [page]
@@ -593,7 +604,7 @@
(update state :media-refs into xform media-refs)))
(defmethod process-change :add-obj
[data {:keys [id obj page-id component-id frame-id parent-id index ignore-touched]}]
[data {:keys [id obj page-id component-id frame-id parent-id index ignore-touched]} _]
;; NOTE: we only perform hard validation on backend
#?(:clj (validate-shape obj page-id))
@@ -628,7 +639,7 @@
objects))
(defmethod process-change :mod-obj
[data {:keys [page-id component-id] :as change}]
[data {:keys [page-id component-id] :as change} _]
(if page-id
(d/update-in-when data [:pages-index page-id :objects] process-operations change)
(d/update-in-when data [:components component-id :objects] process-operations change)))
@@ -658,19 +669,19 @@
objects))
(defmethod process-change :reorder-children
[data {:keys [page-id component-id] :as change}]
[data {:keys [page-id component-id] :as change} _]
(if page-id
(d/update-in-when data [:pages-index page-id :objects] process-children-reordering change)
(d/update-in-when data [:components component-id :objects] process-children-reordering change)))
(defmethod process-change :del-obj
[data {:keys [page-id component-id id ignore-touched]}]
[data {:keys [page-id component-id id ignore-touched]} _]
(if page-id
(d/update-in-when data [:pages-index page-id] ctst/delete-shape id ignore-touched)
(d/update-in-when data [:components component-id] ctst/delete-shape id ignore-touched)))
(defmethod process-change :fix-obj
[data {:keys [page-id component-id id] :as params}]
[data {:keys [page-id component-id id] :as params} _]
(letfn [(fix-container [container]
(case (:fix params :broken-children)
:broken-children (ctst/fix-broken-children container id)
@@ -682,7 +693,7 @@
(d/update-in-when data [:components component-id] fix-container))))
(defmethod process-change :reg-objects
[data {:keys [page-id component-id shapes]}]
[data {:keys [page-id component-id shapes]} _]
;; FIXME: Improve performance
(letfn [(reg-objects [objects]
(let [lookup (d/getf objects)
@@ -734,7 +745,7 @@
(defmethod process-change :mov-objects
;; FIXME: ignore-touched is no longer used, so we can consider it deprecated
[data {:keys [parent-id shapes index page-id component-id #_ignore-touched after-shape allow-altering-copies syncing]}]
[data {:keys [parent-id shapes index page-id component-id #_ignore-touched after-shape allow-altering-copies syncing]} _]
(letfn [(calculate-invalid-targets [objects shape-id]
(let [reduce-fn #(into %1 (calculate-invalid-targets objects %2))]
(->> (get-in objects [shape-id :shapes])
@@ -849,7 +860,7 @@
(d/update-in-when data [:components component-id :objects] move-objects))))
(defmethod process-change :add-page
[data {:keys [id name page]}]
[data {:keys [id name page]} _]
(when (and id name page)
(ex/raise :type :conflict
:hint "id+name or page should be provided, never both"))
@@ -859,7 +870,7 @@
(ctpl/add-page data page)))
(defmethod process-change :mod-page
[data {:keys [id] :as params}]
[data {:keys [id] :as params} _]
(d/update-in-when data [:pages-index id]
(fn [page]
(let [name (get params :name)
@@ -889,7 +900,7 @@
(dissoc :pixel-grid-opacity))))))
(defmethod process-change :set-plugin-data
[data {:keys [object-type object-id page-id namespace key value]}]
[data {:keys [object-type object-id page-id namespace key value]} _]
(letfn [(update-fn [data]
(if (some? value)
(assoc-in data [:plugin-data namespace key] value)
@@ -915,83 +926,83 @@
(d/update-in-when data [:components object-id] update-fn))))
(defmethod process-change :del-page
[data {:keys [id]}]
[data {:keys [id]} _]
(ctpl/delete-page data id))
(defmethod process-change :mov-page
[data {:keys [id index]}]
[data {:keys [id index]} _]
(update data :pages d/insert-at-index index [id]))
(defmethod process-change :add-color
[data {:keys [color]}]
[data {:keys [color]} _]
(ctl/add-color data color))
(defmethod process-change :mod-color
[data {:keys [color]}]
[data {:keys [color]} _]
(ctl/set-color data color))
(defmethod process-change :del-color
[data {:keys [id]}]
[data {:keys [id]} _]
(ctl/delete-color data id))
;; -- Media
(defmethod process-change :add-media
[data {:keys [object]}]
[data {:keys [object]} _]
(update data :media assoc (:id object) object))
(defmethod process-change :mod-media
[data {:keys [object]}]
[data {:keys [object]} _]
(d/update-in-when data [:media (:id object)] merge object))
(defmethod process-change :del-media
[data {:keys [id]}]
[data {:keys [id]} _]
(d/update-when data :media dissoc id))
;; -- Components
(defmethod process-change :add-component
[data params]
[data params _]
(ctkl/add-component data params))
(defmethod process-change :mod-component
[data params]
[data params _]
(ctkl/mod-component data params))
(defmethod process-change :del-component
[data {:keys [id skip-undelete? delta]}]
[data {:keys [id skip-undelete? delta]} _]
(ctf/delete-component data id skip-undelete? delta))
(defmethod process-change :restore-component
[data {:keys [id page-id]}]
[data {:keys [id page-id]} _]
(ctf/restore-component data id page-id))
(defmethod process-change :purge-component
[data {:keys [id]}]
[data {:keys [id]} _]
(ctf/purge-component data id))
;; -- Typography
(defmethod process-change :add-typography
[data {:keys [typography]}]
[data {:keys [typography]} _]
(ctyl/add-typography data typography))
(defmethod process-change :mod-typography
[data {:keys [typography]}]
[data {:keys [typography]} _]
(ctyl/update-typography data (:id typography) merge typography))
(defmethod process-change :del-typography
[data {:keys [id]}]
[data {:keys [id]} _]
(ctyl/delete-typography data id))
;; -- Design Tokens
(defmethod process-change :set-tokens-lib
[data {:keys [tokens-lib]}]
[data {:keys [tokens-lib]} _]
(assoc data :tokens-lib tokens-lib))
(defmethod process-change :set-token
[data {:keys [set-id token-id attrs]}]
[data {:keys [set-id token-id attrs]} _]
(update data :tokens-lib
(fn [lib]
(let [lib' (ctob/ensure-tokens-lib lib)]
@@ -1008,7 +1019,7 @@
(ctob/make-token (merge prev-token attrs)))))))))
(defmethod process-change :set-token-set
[data {:keys [id attrs]}]
[data {:keys [id attrs]} _]
(update data :tokens-lib
(fn [lib]
(let [lib' (ctob/ensure-tokens-lib lib)]
@@ -1023,7 +1034,7 @@
(ctob/update-set lib' id (fn [_] (ctob/make-token-set attrs))))))))
(defmethod process-change :set-token-theme
[data {:keys [id attrs]}]
[data {:keys [id attrs]} _]
(update data :tokens-lib
(fn [lib]
(let [lib' (ctob/ensure-tokens-lib lib)]
@@ -1041,35 +1052,67 @@
(ctob/make-token-theme (merge prev-token-theme attrs)))))))))
(defmethod process-change :set-active-token-themes
[data {:keys [theme-paths]}]
[data {:keys [theme-paths]} _]
(update data :tokens-lib #(-> % (ctob/ensure-tokens-lib)
(ctob/set-active-themes theme-paths))))
(defmethod process-change :rename-token-set-group
[data {:keys [set-group-path set-group-fname]}]
[data {:keys [set-group-path set-group-fname]} _]
(update data :tokens-lib (fn [lib]
(-> lib
(ctob/ensure-tokens-lib)
(ctob/rename-set-group set-group-path set-group-fname)))))
(defmethod process-change :move-token-set
[data {:keys [from-path to-path before-path before-group] :as changes}]
[data {:keys [from-path to-path before-path before-group] :as changes} _]
(update data :tokens-lib #(-> %
(ctob/ensure-tokens-lib)
(ctob/move-set from-path to-path before-path before-group))))
(defmethod process-change :move-token-set-group
[data {:keys [from-path to-path before-path before-group]}]
[data {:keys [from-path to-path before-path before-group]} _]
(update data :tokens-lib #(-> %
(ctob/ensure-tokens-lib)
(ctob/move-set-group from-path to-path before-path before-group))))
;; === Design Tokens configuration
;; --- Design Tokens configuration
(defmethod process-change :set-base-font-size
[data {:keys [base-font-size]}]
[data {:keys [base-font-size]} _]
(ctf/set-base-font-size data base-font-size))
;; --- Validate Shapes
#?(:clj
(defmethod process-change :validate-shapes
[data _ _]
data))
#?(:cljs
(defmethod process-change :validate-shapes
[data {:keys [page-id shape-ids context]} libraries]
(if libraries
(println "Validating shapes: \n"
" page-id:" (str page-id) "\n"
" shape-ids:" (str shape-ids) "\n"
" context:" context)
(let [file {:data data :id uuid/zero}
errors (reduce (fn [acc shape-id]
(if-let [page (ctpl/get-page data page-id)]
(let [page-errors (val/validate-shape shape-id file page libraries)]
(if (seq page-errors)
(into acc page-errors)
acc))
acc))
[]
shape-ids)]
(when (seq errors)
(ex/raise :type :validation
:code :referential-integrity
:hint (str "error on validating shapes: " context)
:details errors))
data))
data))
;; === Operations
@@ -1203,7 +1203,6 @@
[changes]
(::page-id (meta changes)))
(defn set-text-content
[changes id content prev-content]
(assert-page-id! changes)
@@ -1224,3 +1223,12 @@
(-> changes
(update :redo-changes conj redo-change)
(update :undo-changes conj undo-change))))
;; Validate Shapes
(defn validate-shapes
[changes page-id shape-ids context]
(update changes :redo-changes conj {:type :validate-shapes
:page-id page-id
:shape-ids (vec shape-ids)
:context context}))
+12 -3
View File
@@ -10,7 +10,6 @@
[app.common.data.macros :as dm]
[app.common.exceptions :as ex]
[app.common.files.helpers :as cfh]
[app.common.files.variant :as cfv]
[app.common.path-names :as cpn]
[app.common.schema :as sm]
[app.common.types.component :as ctk]
@@ -569,7 +568,17 @@
objects (:objects page)
file-data (:data file)
first-child (get objects (first shapes))
prop-names (cfv/extract-properties-names first-child file-data)]
extract-properties-names
(fn [shape]
;; Get the names of the properties of the shape's component
(->> shape
(#(ctkl/get-component file-data (:component-id %) true))
:variant-properties
(map :name)))
prop-names (extract-properties-names first-child)]
(run! (fn [child-id]
(when-let [child (get objects child-id)]
(if (not (ctk/is-variant? child))
@@ -583,7 +592,7 @@
(str/ffmt "Main instance in variant % should have the variant-id of the container but has %" (:id child) (:variant-id child))
child file page
:variant-id shape-id))
(when (not= prop-names (cfv/extract-properties-names child file-data))
(when (not= prop-names (extract-properties-names child))
(report-error :invalid-variant-properties
(str/ffmt "Variant % has invalid properties %" (:id child) (vec prop-names))
child file page
+6 -13
View File
@@ -6,8 +6,8 @@
(ns app.common.files.variant
(:require
[app.common.data.macros :as dm]
[app.common.types.component :as ctc]
[app.common.types.components-list :as ctcl]
[app.common.types.component :as ctk]
[app.common.types.components-list :as ctkl]
[app.common.types.variant :as ctv]))
(defn find-variant-components
@@ -25,16 +25,9 @@
;; We can't simply filter components, because we need to maintain the order
(->> (dm/get-in objects [variant-id :shapes])
(map #(dm/get-in objects [% :component-id]))
(map #(ctcl/get-component data % true))
(map #(ctkl/get-component data % true))
reverse)))
(defn extract-properties-names
[shape data]
(->> shape
(#(ctcl/get-component data (:component-id %) true))
:variant-properties
(map :name)))
(defn extract-properties-values
"Get a map of properties associated to their possible values"
[data objects variant-id]
@@ -74,10 +67,10 @@
(defn get-primary-component
[data component-id]
(when-let [component (ctcl/get-component data component-id)]
(if (ctc/is-variant? component)
(when-let [component (ctkl/get-component data component-id)]
(if (ctk/is-variant? component)
(->> component
(get-primary-variant data)
:component-id
(ctcl/get-component data))
(ctkl/get-component data))
component)))
+31 -5
View File
@@ -290,8 +290,11 @@
duplicated-parent?
(->> ids-map vals (some #(= % (:parent-id first-shape))))
grid-parent?
(and (ctsl/grid-layout? objects (:parent-id first-shape)) (not duplicated-parent?))
changes
(if (and (ctsl/grid-layout? objects (:parent-id first-shape)) (not duplicated-parent?))
(if grid-parent?
(let [target-cell (-> position meta :cell)
[row column]
@@ -313,7 +316,19 @@
changes
(reduce #(pcb/add-object %1 %2 {:ignore-touched true})
changes
(rest new-shapes))]
(rest new-shapes))
ids-to-validate (cond-> [(:id first-shape)]
grid-parent?
(conj (:parent-id first-shape)))
changes (if (seq ids-to-validate)
(pcb/validate-shapes changes
(:id page)
ids-to-validate
(str "generate-instantiate-component: " component-id
" under parent-id" (or parent-id " root")))
changes)]
[new-shape changes])))
@@ -3123,7 +3138,6 @@
;; we calculate a new one because the components will have created new shapes.
ids-map (into {} (map #(vector % (uuid/next))) all-ids)
;; If there is an alt-duplication we change to root
;; For variants so the copy is made as a child of root
;; This is because inside a variant-container can't be a copy
@@ -3135,7 +3149,6 @@
(assoc :parent-id uuid/zero :frame-id uuid/zero)))
shapes)
changes (-> changes
(pcb/with-page page)
(pcb/with-objects all-objects)
@@ -3165,7 +3178,20 @@
(comp
(filter #(= :add-obj (:type %)))
(map #(vector (:old-id %) (-> % :obj :id))))
(:redo-changes changes))]
(:redo-changes changes))
copied-components
(ctn/get-all-instance-roots (:objects page) ids)
ids-to-validate
(map #(get ids-map % %) copied-components)
changes (if (seq ids-to-validate)
(pcb/validate-shapes changes
(:id page)
ids-to-validate
(cond-> (str "generate-duplicate-changes: " ids)))
changes)]
(-> changes
(generate-duplicate-flows shapes page ids-map)
+29 -5
View File
@@ -75,7 +75,7 @@
(reduce check-shape changes mod-obj-changes)))
(defn generate-update-shapes
[changes ids update-fn objects {:keys [attrs changed-sub-attr ignore-tree ignore-touched with-objects? translation?]}]
[changes ids update-fn objects {:keys [attrs changed-sub-attr ignore-tree ignore-touched with-objects? translation? extra-context]}]
(let [changes (reduce
(fn [changes id]
(let [opts {:attrs attrs
@@ -96,7 +96,19 @@
(pcb/reorder-grid-children ids))
(not ignore-touched)
(generate-unapply-tokens objects changed-sub-attr))]
(generate-unapply-tokens objects changed-sub-attr))
page-id (pcb/get-page-id changes)
modified-components (ctn/get-all-instance-roots objects ids)
changes (if (and page-id (seq modified-components))
(pcb/validate-shapes changes
page-id
modified-components
(cond-> (str "generate-update-shapes: " ids " " attrs)
(some? extra-context)
(str " \n -> from " extra-context)))
changes)]
changes))
(defn- generate-update-shape-flags
@@ -248,8 +260,8 @@
page-id (pcb/get-page-id changes)
page (or (pcb/get-page changes)
(ctpl/get-page data page-id))
ids (cfh/clean-loops objects ids)
in-component-copy?
(fn [shape-id]
;; Look for shapes that are inside a component copy, but are
@@ -258,7 +270,7 @@
;; If we want to specifically allow altering the copies, this is
;; a special case, like a component swap, in which case we want
;; to delete the old shape
(let [shape (get objects shape-id)]
(let [shape (get objects shape-id)]
(and (ctn/has-any-copy-parent? objects shape)
(not allow-altering-copies))))
@@ -437,7 +449,19 @@
(into []
(remove #(and (ctsi/has-destination %)
(id-to-delete? (:destination %))))
interactions))))))]
interactions))))))
modified-components (ctn/get-all-instance-roots objects (disj all-parents uuid/zero))
;; There is no need to validate deleted objects. Probably also no need to validate hidden or unmasked objects,
;; but we may think of it
changes (if (seq modified-components)
(pcb/validate-shapes changes
page-id
modified-components
(str "generate-delete-shapes: " ids))
changes)]
[all-parents changes])))
@@ -172,10 +172,10 @@
new-props (- min-props
(+ (count props)
(if add-name? 1 0)))
props (ctv/add-new-props props (repeat new-props ""))]
props (ctv/add-new-properties props (repeat new-props ""))]
(if add-name?
(ctv/add-new-prop props (:name component))
(ctv/add-new-property props (:name component))
props)))
(defn- create-new-properties-from-non-variant
@@ -216,6 +216,41 @@
:else
(get-instance-root objects (get objects (:parent-id shape)))))
(defn get-all-instance-roots
"Given a list of shape ids and an objects tree, returns a set with the ids of
all instance roots that are at, above or below any of the shapes identified by
the given list. An instance root is a shape that has :component-root set to
true (checked by ctk/instance-root?). There is at most one instance root in
any subtree rooted at an instance root, so the downward search stops at the
first instance root found in each branch. Uses a visited set to avoid
reprocessing the same shapes."
[objects shape-ids]
(let [visited (atom #{})
result (atom #{})]
(letfn [(search-up [shape-id]
(when-not (contains? @visited shape-id)
(swap! visited conj shape-id)
(let [shape (get objects shape-id)]
(when-not (nil? shape)
(if (ctk/instance-root? shape)
(swap! result conj (:id shape))
(when-not (cfh/root? shape)
(when-let [parent-id (:parent-id shape)]
(search-up parent-id))))))))
(search-down [shape-id]
(when-not (contains? @visited shape-id)
(swap! visited conj shape-id)
(let [shape (get objects shape-id)]
(when-not (nil? shape)
(if (ctk/instance-root? shape)
(swap! result conj (:id shape))
(doseq [child-id (:shapes shape)]
(search-down child-id)))))))]
(doseq [shape-id shape-ids]
(search-up shape-id)
(search-down shape-id))
@result)))
(defn find-component-main
"If the shape is a component main instance or is inside one, return that instance.
Uses an iterative loop with cycle detection to prevent stack overflow on circular
+217 -88
View File
@@ -27,6 +27,9 @@
[:variant-id {:optional true} ::sm/uuid]
[:variant-properties {:optional true} [:vector schema:variant-property]]])
(def valid-variant-component?
(sm/check-fn schema:variant-component))
(def schema:variant-shape
"The root shape of the main instance of a variant component"
[:map
@@ -34,14 +37,17 @@
[:variant-name {:optional true} :string]
[:variant-error {:optional true} :string]])
(def valid-variant-shape?
(sm/check-fn schema:variant-shape))
(def schema:variant-container
"Is a board that contains all variant components of a variant set,
for grouping them visually in the workspace"
[:map
[:is-variant-container {:optional true} :boolean]])
(def valid-variant-component?
(sm/check-fn schema:variant-component))
(def valid-variant-container?
(sm/check-fn schema:variant-container))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@@ -51,7 +57,12 @@
(def value-prefix "Value ")
(defn properties-to-name
"Transform the properties into a name, with the values separated by comma"
"Transform the properties into a name, with the values separated by comma.
Example:
[{:name 'Property 1' :value 'Button'}
{:name 'Property 2' :value 'Primary'}] -> 'Button, Primary'"
[properties]
(->> properties
(map :value)
@@ -59,7 +70,12 @@
(str/join ", ")))
(defn next-property-number
"Returns the next property number, to avoid duplicates on the property names"
"Returns the next property number, to avoid duplicates on the property names.
Example:
[{:name 'Property 1' :value 'x'}
{:name 'Property 3' :value 'y'}] -> 4"
[properties]
(let [numbers (keep
#(some->> (:name %) (re-find property-regex) second d/parse-integer)
@@ -69,37 +85,61 @@
0)]
(inc (max max-num (count properties)))))
(defn add-new-prop
"Adds a new property with generated name and provided value to the existing props list."
[props value]
(conj props {:name (str property-prefix (next-property-number props))
:value value}))
(defn add-new-property
"Adds a new property with generated name and provided value to the existing properties list.
(defn add-new-props
"Adds new properties with generated names and provided values to the existing props list."
[props values]
(let [next-prop-num (next-property-number props)
Example:
[{:name 'Property 1' :value 'x'}] 'y' -> [{:name 'Property 1' :value 'x'}
{:name 'Property 2' :value 'y'}]"
[properties value]
(conj properties {:name (str property-prefix (next-property-number properties))
:value value}))
(defn add-new-properties
"Adds new properties with generated names and provided values to the existing properties list.
Example:
[{:name 'Property 1' :value 'x'}] ['a' 'b'] -> [{:name 'Property 1' :value 'x'}
{:name 'Property 2' :value 'a'}
{:name 'Property 3' :value 'b'}]"
[properties values]
(let [next-prop-num (next-property-number properties)
xf (map-indexed (fn [i v]
{:name (str property-prefix (+ next-prop-num i))
:value v}))]
(into props xf values)))
(into properties xf values)))
(defn path-to-properties
"From a list of properties and a name with path, assign each token of the
path as value of a different property"
path as value of a different property. It can add blank properties if
necessary, until the min-properties number is reached.
Example with min-properties=4:
'Button / Primary / Hover' -> [{:name 'Property 1' :value 'Button'}
{:name 'Property 2' :value 'Primary'}
{:name 'Property 3' :value 'Hover'}
{:name 'Property 4' :value ''}]"
([path properties]
(path-to-properties path properties 0))
([path properties min-props]
([path properties min-properties]
(let [cpath (cpn/split-path path)
total-props (max (count cpath) min-props)
total-properties (max (count cpath) min-properties)
assigned (mapv #(assoc % :value (nth cpath %2 "")) properties (range))
;; Add empty strings to the end of cpath to reach the minimum number of properties
cpath (take total-props (concat cpath (repeat "")))
cpath (take total-properties (concat cpath (repeat "")))
remaining (drop (count properties) cpath)]
(add-new-props assigned remaining))))
(add-new-properties assigned remaining))))
(defn properties-map->formula
"Transforms a map of properties to a formula of properties omitting the empty ones"
"Transforms a map of properties to a formula of properties omitting the empty ones.
Example:
[{:name 'Property 1' :value 'Button'}
{:name 'Property 2' :value 'Primary'}] -> 'Property 1=Button, Property 2=Primary'"
[properties]
(->> properties
(keep (fn [{:keys [name value]}]
@@ -108,7 +148,12 @@
(str/join ", ")))
(defn properties-formula->map
"Transforms a formula of properties to a map of properties"
"Transforms a formula of properties to a map of properties.
Example:
'Property 1=Button, Property 2=Primary' -> [{:name 'Property 1' :value 'Button'}
{:name 'Property 2' :value 'Primary'}]"
[s]
(->> (str/split s ",")
(mapv #(str/split % "=" 2))
@@ -118,7 +163,12 @@
:value (str/trim v)}))))
(defn valid-properties-formula?
"Checks if a formula is valid"
"Checks if a formula is valid.
Example:
'Property 1=Button, Property 2=Primary' -> true
'Property 1=Button, Property 2' -> false"
[s]
(->> (str/split s ",")
(mapv #(str/split % "=" 2))
@@ -128,22 +178,41 @@
(< (count (second %)) property-max-length)))))
(defn find-properties-to-remove
"Compares two property maps to find which properties should be removed"
[prev-props upd-props]
(let [upd-names (set (map :name upd-props))]
(filterv #(not (contains? upd-names (:name %))) prev-props)))
"Compares two property maps to find which properties should be removed.
Example:
[{:name 'Property 1' :value 'x'}
{:name 'Property 2' :value 'y'}]
[{:name 'Property 1' :value 'x'}] -> [{:name 'Property 2' :value 'y'}]"
[prev-properties upd-properties]
(let [upd-names (set (map :name upd-properties))]
(filterv #(not (contains? upd-names (:name %))) prev-properties)))
(defn find-properties-to-update
"Compares two property maps to find which properties should be updated"
[prev-props upd-props]
"Compares two property maps to find which properties should be updated.
Example:
[{:name 'Property 1' :value 'x'}
{:name 'Property 2' :value 'y'}]
[{:name 'Property 1' :value 'new-x'}
{:name 'Property 2' :value 'y'}] -> [{:name 'Property 1' :value 'new-x'}]"
[prev-properties upd-properties]
(filterv #(some (fn [prop] (and (= (:name %) (:name prop))
(not= (:value %) (:value prop)))) prev-props) upd-props))
(not= (:value %) (:value prop)))) prev-properties) upd-properties))
(defn find-properties-to-add
"Compares two property maps to find which properties should be added"
[prev-props upd-props]
(let [prev-names (set (map :name prev-props))]
(filterv #(not (contains? prev-names (:name %))) upd-props)))
"Compares two property maps to find which properties should be added.
Example:
[{:name 'Property 1' :value 'x'}]
[{:name 'Property 1' :value 'x'}
{:name 'Property 2' :value 'y'}] -> [{:name 'Property 2' :value 'y'}]"
[prev-properties upd-properties]
(let [prev-names (set (map :name prev-properties))]
(filterv #(not (contains? prev-names (:name %))) upd-properties)))
(defn- split-base-name-and-number
"Extract the number in parentheses from an item, if present, and return both the base name and the number"
@@ -165,7 +234,12 @@
(defn update-number-in-repeated-item
"Add, keep or update a number in parentheses for a given item, if necessary, depending on the items
already present in a list, to avoid repetitions"
already present in a list, to avoid repetitions.
Example:
['Property'] 'Property' -> 'Property (1)'
['Property' 'Property (1)'] 'Property' -> 'Property (2)'"
[items item]
(let [names (group-numbers-by-base-name items)
[base num] (split-base-name-and-number item)
@@ -176,24 +250,40 @@
(str base (when (pos? n) (str " (" n ")")))))))
(defn update-number-in-repeated-prop-names
"Add, keep or update a number for each prop name depending on the previous ones"
[props]
(->> props
"Add, keep or update a number for each prop name depending on the previous ones.
Example:
[{:name 'Property' :value 'x'}
{:name 'Property' :value 'y'}] -> [{:name 'Property' :value 'x'}
{:name 'Property (1)' :value 'y'}]"
[properties]
(->> properties
(reduce (fn [acc prop]
(conj acc {:name (update-number-in-repeated-item (mapv :name acc) (:name prop))
:value (:value prop)}))
[])))
(defn find-index-for-property-name
"Finds the index of a name in a property map"
[props name]
"Finds the index of a name in a property map.
Example:
[{:name 'Property 1' :value 'x'}
{:name 'Property 2' :value 'y'}] 'Property 2' -> 1"
[properties name]
(some (fn [[idx prop]]
(when (= (:name prop) name)
idx))
(map-indexed vector props)))
(map-indexed vector properties)))
(defn remove-prefix
"Removes the given prefix (with or without a trailing ' / ') from the beginning of the name"
"Removes the given prefix (with or without a trailing ' / ') from the beginning of the name.
Example:
'Button / Primary' 'Button' -> 'Primary'
'Button / Primary' 'Other' -> 'Button / Primary'"
[name prefix]
(let [long-name (str prefix " / ")]
(cond
@@ -210,22 +300,22 @@
(map :name))
(defn- matching-indices
[props1 props2]
(let [names-in-p2 (into #{} xf:map-name props2)
[properties1 properties2]
(let [names-in-p2 (into #{} xf:map-name properties2)
xform (comp
(map-indexed (fn [index {:keys [name]}]
(when (contains? names-in-p2 name)
index)))
(filter some?))]
(into #{} xform props1)))
(into #{} xform properties1)))
(defn- find-index-by-name
"Returns the index of the first item in props with the given name, or nil if not found."
[name props]
"Returns the index of the first item in properties with the given name, or nil if not found."
[name properties]
(some (fn [[idx item]]
(when (= (:name item) name)
idx))
(map-indexed vector props)))
(map-indexed vector properties)))
(defn- next-valid-position
"Returns the first non-negative integer not present in the used-pos set."
@@ -236,42 +326,60 @@
p)))
(defn- find-position
"Returns the index of the property with the given name in `props`,
"Returns the index of the property with the given name in `properties`,
or the next available index not in `used-pos` if not found."
[name props used-pos]
(or (find-index-by-name name props)
[name properties used-pos]
(or (find-index-by-name name properties)
(next-valid-position used-pos)))
(defn merge-properties
"Merges props2 into props1 with the following rules:
- For each property p2 in props2:
"Merges properties2 into properties1 with the following rules:
- For each property p2 in properties2:
- Skip it if its value is empty.
- If props1 contains a property with the same name, update its value with that of p2.
- Otherwise, assign p2's value to the first unused property in props1. A property is considered used if:
- Its name exists in both props1 and props2, or
- If properties1 contains a property with the same name, update its value with that of p2.
- Otherwise, assign p2's value to the first unused property in properties1. A property is considered used if:
- Its name exists in both properties1 and properties2, or
- Its value has already been updated during the merge.
- If no unused properties are available in props1, append a new property with a default name and p2's value."
[props1 props2]
(let [props2 (remove #(str/empty? (:value %)) props2)]
- If no unused properties are available in properties1, append a new property with a default name and p2's value.
Example:
[{:name 'Property 1' :value 'a'}
{:name 'Property 2' :value 'b'}]
[{:name 'Property 1' :value 'x'}
{:name 'Property 2' :value 'y'}
{:name 'Property 3' :value 'z'}] -> [{:name 'Property 1' :value 'x'}
{:name 'Property 2' :value 'y'}
{:name 'Property 3' :value 'z'}]"
[properties1 properties2]
(let [properties2 (remove #(str/empty? (:value %)) properties2)]
(-> (reduce
(fn [{:keys [props used-pos]} prop]
(let [pos (find-position (:name prop) props used-pos)
(fn [{:keys [properties used-pos]} prop]
(let [pos (find-position (:name prop) properties used-pos)
used-pos (conj used-pos pos)]
(if (< pos (count props))
{:props (assoc-in (vec props) [pos :value] (:value prop)) :used-pos used-pos}
{:props (add-new-prop props (:value prop)) :used-pos used-pos})))
{:props (vec props1) :used-pos (matching-indices props1 props2)}
props2)
:props)))
(if (< pos (count properties))
{:properties (assoc-in (vec properties) [pos :value] (:value prop)) :used-pos used-pos}
{:properties (add-new-property properties (:value prop)) :used-pos used-pos})))
{:properties (vec properties1) :used-pos (matching-indices properties1 properties2)}
properties2)
:properties)))
(defn compare-properties
"Compares vectors of properties keeping the value if it is the same for all
or setting a custom value where their values do not coincide"
([props-list]
(compare-properties props-list nil))
or setting a custom value where their values do not coincide.
([props-list distinct-mark]
(let [grouped (group-by :name (apply concat props-list))
Example:
[[{:name 'Property 1' :value 'x'}
{:name 'Property 2' :value 'y'}]
[{:name 'Property 1' :value 'x'}
{:name 'Property 2' :value 'z'}]] -> [{:name 'Property 1' :value 'x'}
{:name 'Property 2' :value nil}]"
([properties-list]
(compare-properties properties-list nil))
([properties-list distinct-mark]
(let [grouped (group-by :name (apply concat properties-list))
check-values (fn [values]
(let [vals (map :value values)]
(if (apply = vals)
@@ -281,33 +389,35 @@
{:name name :value (check-values values)})
grouped))))
(defn same-variant?
"Determines if all elements belong to the same variant"
[components]
(let [variant-ids (distinct (map :variant-id components))
not-blank? (complement str/blank?)]
(and
(= 1 (count variant-ids))
(not-blank? (first variant-ids)))))
(defn properties-distance
"Computes a weighted distance between two property lists `properties1` and `properties2`.
Latter properties weight less that previous ones.
(defn distance
"Computes a weighted distance between two property lists `props1` and `props2`.
Latter properties weight less that previous ones"
[props1 props2]
(let [total-num-props (count props1)
Example:
[{:name 'type' :value 'primary'}
{:name 'status' :value 'default'}]
[{:name 'type' :value 'primary'}
{:name 'status' :value 'hover'}] -> 1.0"
[properties1 properties2]
(let [total-num-properties (count properties1)
xform (map-indexed
(fn [idx [p1 p2]]
(if (not= p1 p2)
(math/pow 2 (- total-num-props idx))
(math/pow 2 (- total-num-properties idx))
0)))]
(transduce
xform
+
(map vector props1 props2))))
(map vector properties1 properties2))))
(defn variant-name-to-name
"Transforms a variant-name (its properties values) into a standard name:
the real name of the shape joined by the properties values separated by '/'"
the real name of the shape joined by the properties values separated by '/'.
Example:
{:name 'Button' :variant-name 'Primary, Hover'} -> 'Button / Primary / Hover'"
[variant]
(cpn/merge-path-item (:name variant) (str/replace (:variant-name variant) #", " " / ")))
@@ -318,7 +428,12 @@
(defn find-boolean-pair
"Given a vector, return a map that contains the boolean equivalency if the values match
with any of the boolean pairs. Returns nil if none match."
with any of the boolean pairs. Returns nil if none match.
Example:
['on' 'off'] -> {'on' true 'off' false}
['foo' 'bar'] -> nil"
[[a b :as v]]
(let [a' (-> a str/trim str/lower)
b' (-> b str/trim str/lower)]
@@ -330,3 +445,17 @@
(= a' f)) {b true a false}
:else nil))
boolean-pairs))))
(defn same-variant?
"Determines if all elements belong to the same variant.
Example:
[{:variant-id 'abc'} {:variant-id 'abc'}] -> true
[{:variant-id 'abc'} {:variant-id 'def'}] -> false"
[components]
(let [variant-ids (distinct (map :variant-id components))
not-blank? (complement str/blank?)]
(and
(= 1 (count variant-ids))
(not-blank? (first variant-ids)))))
+2
View File
@@ -87,6 +87,7 @@
[common-tests.types.token-test]
[common-tests.types.tokens-lib-test]
[common-tests.types.tokens-status-test]
[common-tests.types.variant-test]
[common-tests.undo-stack-test]
[common-tests.uuid-test]))
@@ -142,6 +143,7 @@
'common-tests.logic.token-test
'common-tests.logic.variants-switch-test
'common-tests.math-test
'common-tests.types.variant-test
'common-tests.media-test
'common-tests.path-names-test
'common-tests.record-test
+228 -10
View File
@@ -9,8 +9,220 @@
[app.common.types.variant :as ctv]
[clojure.test :as t]))
(t/deftest properties-to-name-test
(t/is (= "" (ctv/properties-to-name [])))
(t/is (= "" (ctv/properties-to-name nil)))
(t/is (= "Button, Primary" (ctv/properties-to-name [{:name "Property 1" :value "Button"}
{:name "Property 2" :value "Primary"}])))
(t/is (= "Button" (ctv/properties-to-name [{:name "Property 1" :value "Button"}
{:name "Property 2" :value ""}]))))
(t/deftest variant-distance01
(t/deftest next-property-number-test
(t/is (= 1 (ctv/next-property-number [])))
(t/is (= 1 (ctv/next-property-number nil)))
(t/is (= 2 (ctv/next-property-number [{:name "Property 1" :value "x"}])))
(t/is (= 4 (ctv/next-property-number [{:name "Property 3" :value "x"}])))
(t/is (= 3 (ctv/next-property-number [{:name "Property 1" :value "x"}
{:name "Property 2" :value "y"}]))))
(t/deftest add-new-property-test
(t/is (= [{:name "Property 1" :value "x"}]
(ctv/add-new-property [] "x")))
(t/is (= [{:name "Property 1" :value "x"}]
(ctv/add-new-property nil "x")))
(t/is (= [{:name "Property 1" :value "x"} {:name "Property 2" :value "y"}]
(ctv/add-new-property [{:name "Property 1" :value "x"}] "y"))))
(t/deftest add-new-properties-test
(t/is (= [{:name "Property 1" :value "a"} {:name "Property 2" :value "b"}]
(ctv/add-new-properties [] ["a" "b"])))
(t/is (= '({:name "Property 2" :value "b"} {:name "Property 1" :value "a"})
(ctv/add-new-properties nil ["a" "b"])))
(t/is (= [{:name "Property 1" :value "x"} {:name "Property 2" :value "a"} {:name "Property 3" :value "b"}]
(ctv/add-new-properties [{:name "Property 1" :value "x"}] ["a" "b"]))))
(t/deftest path-to-properties-test
(t/is (= [] (ctv/path-to-properties "" [])))
(t/is (= [{:name "Property 1" :value "a"} {:name "Property 2" :value "b"}]
(ctv/path-to-properties "a / b" nil)))
(t/is (= [{:name "Property 1" :value "Button"}
{:name "Property 2" :value "Primary"}
{:name "Property 3" :value "Hover"}]
(ctv/path-to-properties "Button / Primary / Hover" [])))
(t/is (= [{:name "Property 1" :value "Button"}
{:name "Property 2" :value "Primary"}
{:name "Property 3" :value "Hover"}
{:name "Property 4" :value ""}]
(ctv/path-to-properties "Button / Primary / Hover" [] 4)))
(t/is (= [{:name "Property 1" :value "Button"}
{:name "Property 2" :value "Primary"}]
(ctv/path-to-properties "Button / Primary" [{:name "Property 1" :value "old"}
{:name "Property 2" :value "old2"}]))))
(t/deftest properties-map->formula-test
(t/is (= "" (ctv/properties-map->formula [])))
(t/is (= "" (ctv/properties-map->formula nil)))
(t/is (= "Property 1=Button, Property 2=Primary"
(ctv/properties-map->formula [{:name "Property 1" :value "Button"}
{:name "Property 2" :value "Primary"}])))
(t/is (= "Property 1=Button"
(ctv/properties-map->formula [{:name "Property 1" :value "Button"}
{:name "Property 2" :value ""}]))))
(t/deftest properties-formula->map-test
(t/is (= [] (ctv/properties-formula->map "")))
(t/is (= [] (ctv/properties-formula->map nil)))
(t/is (= [{:name "Property 1" :value "Button"} {:name "Property 2" :value "Primary"}]
(ctv/properties-formula->map "Property 1=Button, Property 2=Primary")))
(t/is (= [{:name "Property 1" :value "Button"}]
(ctv/properties-formula->map "Property 1=Button, Property 2="))))
(t/deftest valid-properties-formula?-test
(t/is (= true (ctv/valid-properties-formula? "Property 1=Button, Property 2=Primary")))
(t/is (= false (ctv/valid-properties-formula? "")))
(t/is (= true (ctv/valid-properties-formula? nil)))
(t/is (= false (ctv/valid-properties-formula? "Property 1=Button, Property 2"))))
(t/deftest find-properties-to-remove-test
(t/is (= [] (ctv/find-properties-to-remove [] [])))
(t/is (= [] (ctv/find-properties-to-remove nil nil)))
(t/is (= [{:name "Property 3" :value "z"}]
(ctv/find-properties-to-remove [{:name "Property 1" :value "x"}
{:name "Property 2" :value "y"}
{:name "Property 3" :value "z"}]
[{:name "Property 1" :value "x"}
{:name "Property 2" :value "y"}])))
(t/is (= [{:name "Property 1" :value "x"} {:name "Property 2" :value "y"}]
(ctv/find-properties-to-remove [{:name "Property 1" :value "x"}
{:name "Property 2" :value "y"}]
[{:name "Property 3" :value "z"}]))))
(t/deftest find-properties-to-update-test
(t/is (= [] (ctv/find-properties-to-update [] [])))
(t/is (= [] (ctv/find-properties-to-update nil nil)))
(t/is (= [{:name "Property 1" :value "new-x"}]
(ctv/find-properties-to-update [{:name "Property 1" :value "x"}
{:name "Property 2" :value "y"}]
[{:name "Property 1" :value "new-x"}
{:name "Property 2" :value "y"}])))
(t/is (= [{:name "Property 1" :value "new-x"} {:name "Property 2" :value "new-y"}]
(ctv/find-properties-to-update [{:name "Property 1" :value "x"}
{:name "Property 2" :value "y"}]
[{:name "Property 1" :value "new-x"}
{:name "Property 2" :value "new-y"}]))))
(t/deftest find-properties-to-add-test
(t/is (= [] (ctv/find-properties-to-add [] [])))
(t/is (= [] (ctv/find-properties-to-add nil nil)))
(t/is (= [{:name "Property 3" :value "z"}]
(ctv/find-properties-to-add [{:name "Property 1" :value "x"}
{:name "Property 2" :value "y"}]
[{:name "Property 1" :value "x"}
{:name "Property 2" :value "y"}
{:name "Property 3" :value "z"}])))
(t/is (= [{:name "Property 2" :value "y"}]
(ctv/find-properties-to-add [{:name "Property 1" :value "x"}]
[{:name "Property 1" :value "x"}
{:name "Property 2" :value "y"}]))))
(t/deftest update-number-in-repeated-item-test
(t/is (= "Property" (ctv/update-number-in-repeated-item [] "Property")))
(t/is (= "Property" (ctv/update-number-in-repeated-item nil "Property")))
(t/is (= "Property (1)" (ctv/update-number-in-repeated-item ["Property"] "Property")))
(t/is (= "Property (2)" (ctv/update-number-in-repeated-item ["Property" "Property (1)"] "Property")))
(t/is (= "Property" (ctv/update-number-in-repeated-item ["Other"] "Property"))))
(t/deftest update-number-in-repeated-prop-names-test
(t/is (= [] (ctv/update-number-in-repeated-prop-names [])))
(t/is (= [] (ctv/update-number-in-repeated-prop-names nil)))
(t/is (= [{:name "Property" :value "x"}]
(ctv/update-number-in-repeated-prop-names [{:name "Property" :value "x"}])))
(t/is (= [{:name "Property" :value "x"} {:name "Property (1)" :value "y"}]
(ctv/update-number-in-repeated-prop-names [{:name "Property" :value "x"}
{:name "Property" :value "y"}])))
(t/is (= [{:name "Property" :value "x"} {:name "Property (1)" :value "y"} {:name "Property (2)" :value "z"}]
(ctv/update-number-in-repeated-prop-names [{:name "Property" :value "x"}
{:name "Property" :value "y"}
{:name "Property" :value "z"}]))))
(t/deftest find-index-for-property-name-test
(t/is (= nil (ctv/find-index-for-property-name [] "Property 1")))
(t/is (= nil (ctv/find-index-for-property-name nil "Property 1")))
(t/is (= 0 (ctv/find-index-for-property-name [{:name "Property 1" :value "x"}] "Property 1")))
(t/is (= 1 (ctv/find-index-for-property-name [{:name "Property 1" :value "x"}
{:name "Property 2" :value "y"}] "Property 2")))
(t/is (= nil (ctv/find-index-for-property-name [{:name "Property 1" :value "x"}] "Property 3"))))
(t/deftest remove-prefix-test
(t/is (= "name" (ctv/remove-prefix "name" "")))
(t/is (= "name" (ctv/remove-prefix "name" nil)))
(t/is (= "Primary" (ctv/remove-prefix "Button / Primary" "Button")))
(t/is (= "Primary" (ctv/remove-prefix "Button / Primary" "Button / ")))
(t/is (= "Button / Primary" (ctv/remove-prefix "Button / Primary" "Other"))))
(t/deftest merge-properties-test
(t/is (= [] (ctv/merge-properties [] [])))
(t/is (= [] (ctv/merge-properties nil nil)))
(t/is (= [{:name "Property 1" :value "x"} {:name "Property 2" :value "y"}]
(ctv/merge-properties [{:name "Property 1" :value "a"}
{:name "Property 2" :value "b"}]
[{:name "Property 1" :value "x"}
{:name "Property 2" :value "y"}])))
(t/is (= [{:name "Property 1" :value "x"} {:name "Property 2" :value "y"} {:name "Property 3" :value "z"}]
(ctv/merge-properties [{:name "Property 1" :value "a"}
{:name "Property 2" :value "b"}]
[{:name "Property 1" :value "x"}
{:name "Property 2" :value "y"}
{:name "Property 3" :value "z"}])))
(t/is (= [{:name "Property 1" :value "a"} {:name "Property 2" :value "y"}]
(ctv/merge-properties [{:name "Property 1" :value "a"}
{:name "Property 2" :value "b"}]
[{:name "Property 2" :value "y"}]))))
(t/deftest compare-properties-test
(t/is (= [] (ctv/compare-properties [])))
(t/is (= [] (ctv/compare-properties nil)))
(t/is (= [{:name "Property 1" :value "x"} {:name "Property 2" :value "y"}]
(ctv/compare-properties [[{:name "Property 1" :value "x"}
{:name "Property 2" :value "y"}]])))
(t/is (= [{:name "Property 1" :value "x"} {:name "Property 2" :value nil}]
(ctv/compare-properties [[{:name "Property 1" :value "x"}
{:name "Property 2" :value "y"}]
[{:name "Property 1" :value "x"}
{:name "Property 2" :value "z"}]])))
(t/is (= [{:name "Property 1" :value "x"} {:name "Property 2" :value "*"}]
(ctv/compare-properties [[{:name "Property 1" :value "x"}
{:name "Property 2" :value "y"}]
[{:name "Property 1" :value "x"}
{:name "Property 2" :value "z"}]]
"*"))))
(t/deftest variant-name-to-name-test
(t/is (= "Button / Primary / Hover" (ctv/variant-name-to-name {:name "Button" :variant-name "Primary, Hover"})))
(t/is (= "Button" (ctv/variant-name-to-name {:name "Button" :variant-name ""})))
(t/is (= "Button" (ctv/variant-name-to-name {:name "Button" :variant-name nil})))
(t/is (= "" (ctv/variant-name-to-name {:name "" :variant-name ""})))
(t/is (= nil (ctv/variant-name-to-name {:name nil :variant-name nil}))))
(t/deftest find-boolean-pair-test
(t/is (= {"on" true "off" false} (ctv/find-boolean-pair ["on" "off"])))
(t/is (= {"yes" true "no" false} (ctv/find-boolean-pair ["yes" "no"])))
(t/is (= {"true" true "false" false} (ctv/find-boolean-pair ["true" "false"])))
(t/is (= {"on" true "off" false} (ctv/find-boolean-pair ["off" "on"])))
(t/is (= {"ON" true "OFF" false} (ctv/find-boolean-pair ["ON" "OFF"])))
(t/is (= nil (ctv/find-boolean-pair ["foo" "bar"])))
(t/is (= nil (ctv/find-boolean-pair nil)))
(t/is (= nil (ctv/find-boolean-pair ["on"]))))
(t/deftest same-variant?-test
(t/is (= false (ctv/same-variant? [])))
(t/is (= false (ctv/same-variant? nil)))
(t/is (= true (ctv/same-variant? [{:variant-id "abc"}])))
(t/is (= true (ctv/same-variant? [{:variant-id "abc"} {:variant-id "abc"}])))
(t/is (= false (ctv/same-variant? [{:variant-id "abc"} {:variant-id "def"}])))
(t/is (= false (ctv/same-variant? [{:variant-id ""} {:variant-id ""}]))))
(t/deftest properties-distance01
;;c1: primary, default, rounded, blue, dark
;;c2: primary, hover, squared, blue, dark
;;c3: primary, default, squared, blue, light
@@ -35,12 +247,12 @@
{:name "borders" :value "rounded"}
{:name "color" :value "blue"}
{:name "theme" :value "light"}]
dist2 (ctv/distance target props2)
dist3 (ctv/distance target props3)]
dist2 (ctv/properties-distance target props2)
dist3 (ctv/properties-distance target props3)]
(t/is (< dist3 dist2))))
(t/deftest variant-distance02
(t/deftest properties-distance02
;;c1: primary, default, rounded, blue, dark
;;c2: primary, hover, squared, red, dark
;;c3: secondary, hover, rounded, blue, dark
@@ -65,11 +277,11 @@
{:name "borders" :value "rounded"}
{:name "color" :value "blue"}
{:name "theme" :value "dark"}]
dist2 (ctv/distance target props2)
dist3 (ctv/distance target props3)]
dist2 (ctv/properties-distance target props2)
dist3 (ctv/properties-distance target props3)]
(t/is (< dist2 dist3))))
(t/deftest variant-distance03
(t/deftest properties-distance03
;;c1: primary, default, rounded, blue, dark
;;c2: secondary, default, rounded, blue, light
;;c3: secondary, hover, squared, blue, dark
@@ -101,12 +313,18 @@
{:name "borders" :value "rounded"}
{:name "color" :value "blue"}
{:name "theme" :value "dark"}]
dist2 (ctv/distance target props2)
dist3 (ctv/distance target props3)
dist4 (ctv/distance target props4)]
dist2 (ctv/properties-distance target props2)
dist3 (ctv/properties-distance target props3)
dist4 (ctv/properties-distance target props4)]
(t/is (< dist2 dist4))
(t/is (< dist4 dist3))))
(t/deftest properties-distance04
(t/is (= 0 (ctv/properties-distance [] [])))
(t/is (= 0 (ctv/properties-distance nil nil)))
(t/is (= 0 (ctv/properties-distance [{:name "a" :value "x"}] [{:name "a" :value "x"}])))
(t/is (= 2.0 (ctv/properties-distance [{:name "a" :value "x"} {:name "b" :value "y"}] [{:name "a" :value "x"} {:name "b" :value "z"}]))))
+8 -6
View File
@@ -103,12 +103,14 @@
(wasm.api/process-objects shapes)
(wasm.api/request-render "sync-wasm-structural-changes"))))))
(defn- apply-changes-localy
(defn- apply-changes-locally
[{:keys [file-id redo-changes ignore-wasm?] :as commit} pending]
(ptk/reify ::apply-changes-localy
(ptk/reify ::apply-changes-locally
ptk/UpdateEvent
(update [_ state]
(let [undo-changes
(let [libraries (dsh/lookup-libraries state)
undo-changes
(if pending
(->> pending
(map :undo-changes)
@@ -126,8 +128,8 @@
apply-changes
(fn [fdata]
(let [fdata (cpc/process-changes fdata undo-changes false)
fdata (cpc/process-changes fdata redo-changes false)
(let [fdata (cpc/process-changes fdata undo-changes false libraries)
fdata (cpc/process-changes fdata redo-changes false libraries)
pids (into #{} xf:map-page-id redo-changes)]
(reduce #(ctst/update-object-indices %1 %2) fdata pids)))]
@@ -200,7 +202,7 @@
(let [pending (when-not local?
(get-pending-commits state))]
(rx/concat
(rx/of (apply-changes-localy commit pending))
(rx/of (apply-changes-locally commit pending))
(if pending
(rx/concat
(->> (rx/from (reverse pending))
@@ -193,7 +193,8 @@
layout-initializer (get-layout-initializer type from-frame? calculate-params?)]
(rx/of (dwu/start-undo-transaction undo-id)
(dwsh/update-shapes [id] layout-initializer {:with-objects? true})
(dwsh/update-shapes [id] layout-initializer {:with-objects? true
:extra-context (str "create-layout-from-id: " id)})
(dwsh/update-shapes (dm/get-prop parent :shapes) #(dissoc % :constraints-h :constraints-v))
(ptk/data-event :layout/update {:ids [id]})
(dwu/commit-undo-transaction undo-id))))))
@@ -189,7 +189,7 @@
{:as props
:keys [reg-objects? save-undo? stack-undo? attrs ignore-tree page-id
ignore-touched undo-group with-objects? changed-sub-attr translation?
skip-component-sync?]
skip-component-sync? extra-context]
:or {reg-objects? false
save-undo? true
stack-undo? false
@@ -222,7 +222,8 @@
:ignore-tree ignore-tree
:ignore-touched ignore-touched
:with-objects? with-objects?
:translation? translation?})
:translation? translation?
:extra-context extra-context})
(cond-> undo-group
(pcb/set-undo-group undo-group))
(pcb/set-translation? translation?)
@@ -764,7 +764,7 @@
(remove #(= (:id %) component-id))
(filter #(= (dm/get-in % [:variant-properties pos :value]) val))
(reverse))
nearest-comp (apply min-key #(ctv/distance target-props (:variant-properties %)) valid-comps)
nearest-comp (apply min-key #(ctv/properties-distance target-props (:variant-properties %)) valid-comps)
shape-parents (cfh/get-parents-with-self current-page-objects (:parent-id shape))
nearest-comp-children (cfh/get-children-with-self component-page-objects (:main-instance-id nearest-comp))
comps-nesting-loop? (seq? (cfh/components-nesting-loop? nearest-comp-children shape-parents))