mirror of
https://github.com/penpot/penpot.git
synced 2025-05-20 20:26:14 +02:00
✨ Copy values of same named properties moving a variant into another (#6288)
* ✨ Copy values of same named properties moving a variant into another * ✨ Add MR changes
This commit is contained in:
parent
ec8c30f060
commit
f4b16a255c
5 changed files with 163 additions and 80 deletions
|
@ -436,7 +436,7 @@
|
||||||
child file page))
|
child file page))
|
||||||
(when (not= prop-names (cfv/extract-properties-names child (:data file)))
|
(when (not= prop-names (cfv/extract-properties-names child (:data file)))
|
||||||
(report-error :invalid-variant-properties
|
(report-error :invalid-variant-properties
|
||||||
(str/ffmt "Variant % has invalid properties" (:id child))
|
(str/ffmt "Variant % has invalid properties %" (:id child) (vec prop-names))
|
||||||
child file page)))))))
|
child file page)))))))
|
||||||
|
|
||||||
(defn- check-variant
|
(defn- check-variant
|
||||||
|
|
|
@ -117,86 +117,83 @@
|
||||||
[changes shapes]
|
[changes shapes]
|
||||||
(reduce generate-make-shape-no-variant changes shapes))
|
(reduce generate-make-shape-no-variant changes shapes))
|
||||||
|
|
||||||
|
|
||||||
|
(defn- generate-new-properties-from-variant
|
||||||
|
[shape min-props data container-name base-properties]
|
||||||
|
(let [component (ctcl/get-component data (:component-id shape) true)
|
||||||
|
add-name? (not= (:name component) container-name)
|
||||||
|
props (ctv/merge-properties base-properties
|
||||||
|
(:variant-properties component))
|
||||||
|
new-props (- min-props
|
||||||
|
(+ (count props)
|
||||||
|
(if add-name? 1 0)))
|
||||||
|
props (ctv/add-new-props props (repeat new-props ""))]
|
||||||
|
|
||||||
|
(if add-name?
|
||||||
|
(ctv/add-new-prop props (:name component))
|
||||||
|
props)))
|
||||||
|
|
||||||
|
(defn- generate-new-properties-from-non-variant
|
||||||
|
[shape min-props container-name base-properties]
|
||||||
|
(let [;; Remove container name from shape name if present
|
||||||
|
shape-name (ctv/remove-prefix (:name shape) container-name)]
|
||||||
|
(ctv/path-to-properties shape-name base-properties min-props)))
|
||||||
|
|
||||||
|
|
||||||
(defn generate-make-shapes-variant
|
(defn generate-make-shapes-variant
|
||||||
[changes shapes variant-container]
|
[changes shapes variant-container]
|
||||||
(let [data (pcb/get-library-data changes)
|
(let [data (pcb/get-library-data changes)
|
||||||
objects (pcb/get-objects changes)
|
objects (pcb/get-objects changes)
|
||||||
|
variant-id (:id variant-container)
|
||||||
container-name (:name variant-container)
|
|
||||||
long-name (str container-name " / ")
|
|
||||||
|
|
||||||
get-base-name (fn [shape]
|
|
||||||
(let [component (ctcl/get-component data (:component-id shape) true)
|
|
||||||
|
|
||||||
name (if (some? (:variant-name shape))
|
|
||||||
(str (:name component)
|
|
||||||
" / "
|
|
||||||
(str/replace (:variant-name shape) #", " " / "))
|
|
||||||
(:name shape))]
|
|
||||||
;; When the name starts by the same name that the container,
|
|
||||||
;; we should ignore that part of the name
|
|
||||||
(cond
|
|
||||||
(str/starts-with? name long-name)
|
|
||||||
(subs name (count long-name))
|
|
||||||
|
|
||||||
(str/starts-with? name container-name)
|
|
||||||
(subs name (count container-name))
|
|
||||||
|
|
||||||
:else
|
|
||||||
name)))
|
|
||||||
|
|
||||||
calc-num-props #(-> %
|
|
||||||
get-base-name
|
|
||||||
cfh/split-path
|
|
||||||
count)
|
|
||||||
|
|
||||||
max-path-items (apply max (map calc-num-props shapes))
|
|
||||||
|
|
||||||
;; If we are cut-pasting a variant-container, this will be null
|
;; If we are cut-pasting a variant-container, this will be null
|
||||||
;; because it hasn't any shapes yet
|
;; because it hasn't any shapes yet
|
||||||
first-comp-id (->> variant-container
|
first-comp-id (->> variant-container
|
||||||
:shapes
|
:shapes
|
||||||
first
|
first
|
||||||
(get objects)
|
(get objects)
|
||||||
:component-id)
|
:component-id)
|
||||||
|
|
||||||
variant-properties (get-in data [:components first-comp-id :variant-properties])
|
base-props (->> (get-in data [:components first-comp-id :variant-properties])
|
||||||
num-props (count variant-properties)
|
(map #(assoc % :value "")))
|
||||||
num-new-props (if (or (nil? first-comp-id)
|
num-base-props (count base-props)
|
||||||
(< max-path-items num-props))
|
|
||||||
0
|
|
||||||
(- max-path-items num-props))
|
|
||||||
total-props (+ num-props num-new-props)
|
|
||||||
|
|
||||||
changes (nth
|
[cpath cname] (cfh/parse-path-name (:name variant-container))
|
||||||
(iterate #(generate-add-new-property % (:id variant-container)) changes)
|
container-name (:name variant-container)
|
||||||
num-new-props)
|
|
||||||
|
|
||||||
changes (pcb/update-shapes changes (map :id shapes)
|
generate-new-properties
|
||||||
#(assoc % :variant-id (:id variant-container)
|
(fn [shape min-props]
|
||||||
:name (:name variant-container)))]
|
(if (ctk/is-variant? shape)
|
||||||
|
(generate-new-properties-from-variant shape min-props data container-name base-props)
|
||||||
|
(generate-new-properties-from-non-variant shape min-props container-name base-props)))
|
||||||
|
|
||||||
|
total-props (reduce (fn [m shape]
|
||||||
|
(max m (count (generate-new-properties shape num-base-props))))
|
||||||
|
0
|
||||||
|
shapes)
|
||||||
|
|
||||||
|
num-new-props (if (or (zero? num-base-props)
|
||||||
|
(< total-props num-base-props))
|
||||||
|
0
|
||||||
|
(- total-props num-base-props))
|
||||||
|
|
||||||
|
changes (nth
|
||||||
|
(iterate #(generate-add-new-property % variant-id) changes)
|
||||||
|
num-new-props)
|
||||||
|
|
||||||
|
changes (pcb/update-shapes changes (map :id shapes)
|
||||||
|
#(assoc % :variant-id variant-id
|
||||||
|
:name (:name variant-container)))]
|
||||||
(reduce
|
(reduce
|
||||||
(fn [changes shape]
|
(fn [changes shape]
|
||||||
(if (or (nil? first-comp-id)
|
(if (or (zero? num-base-props)
|
||||||
(= (:id variant-container) (:variant-id shape)))
|
(= variant-id (:variant-id shape)))
|
||||||
changes ;; do nothing if we aren't changing the parent
|
changes ;; do nothing more if we aren't changing the parent or there are no base props
|
||||||
(let [base-name (get-base-name shape)
|
(let [props (generate-new-properties shape total-props)
|
||||||
|
variant-name (ctv/properties-to-name props)]
|
||||||
;; we need to get the updated library data to have access to the current properties
|
|
||||||
data (pcb/get-library-data changes)
|
|
||||||
|
|
||||||
props (ctv/path-to-properties
|
|
||||||
base-name
|
|
||||||
(get-in data [:components first-comp-id :variant-properties])
|
|
||||||
total-props)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
variant-name (ctv/properties-to-name props)
|
|
||||||
[cpath cname] (cfh/parse-path-name (:name variant-container))]
|
|
||||||
(-> (pcb/update-component changes
|
(-> (pcb/update-component changes
|
||||||
(:component-id shape)
|
(:component-id shape)
|
||||||
#(assoc % :variant-id (:id variant-container)
|
#(assoc % :variant-id variant-id
|
||||||
:variant-properties props
|
:variant-properties props
|
||||||
:name cname
|
:name cname
|
||||||
:path cpath)
|
:path cpath)
|
||||||
|
@ -204,5 +201,4 @@
|
||||||
(pcb/update-shapes [(:id shape)]
|
(pcb/update-shapes [(:id shape)]
|
||||||
#(assoc % :variant-name variant-name))))))
|
#(assoc % :variant-name variant-name))))))
|
||||||
changes
|
changes
|
||||||
shapes)))
|
shapes)))
|
||||||
|
|
|
@ -74,6 +74,20 @@
|
||||||
0)]
|
0)]
|
||||||
(inc (max max-num (count properties)))))
|
(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-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)
|
||||||
|
xf (map-indexed (fn [i v]
|
||||||
|
{:name (str property-prefix (+ next-prop-num i))
|
||||||
|
:value v}))]
|
||||||
|
(into props xf values)))
|
||||||
|
|
||||||
(defn path-to-properties
|
(defn path-to-properties
|
||||||
"From a list of properties and a name with path, assign each token of the
|
"From a list of properties and a name with path, assign each token of the
|
||||||
|
@ -81,15 +95,13 @@
|
||||||
([path properties]
|
([path properties]
|
||||||
(path-to-properties path properties 0))
|
(path-to-properties path properties 0))
|
||||||
([path properties min-props]
|
([path properties min-props]
|
||||||
(let [next-prop-num (next-property-number properties)
|
(let [cpath (cfh/split-path path)
|
||||||
cpath (cfh/split-path path)
|
total-props (max (count cpath) min-props)
|
||||||
assigned (mapv #(assoc % :value (nth cpath %2 "")) properties (range))
|
assigned (mapv #(assoc % :value (nth cpath %2 "")) properties (range))
|
||||||
;; Add empty strings to the end of path to reach the minimum number of properties
|
;; Add empty strings to the end of cpath to reach the minimum number of properties
|
||||||
cpath (take min-props (concat path (repeat "")))
|
cpath (take total-props (concat cpath (repeat "")))
|
||||||
remaining (drop (count properties) cpath)
|
remaining (drop (count properties) cpath)]
|
||||||
new-properties (map-indexed (fn [i v] {:name (str property-prefix (+ next-prop-num i))
|
(add-new-props assigned remaining))))
|
||||||
:value v}) remaining)]
|
|
||||||
(into assigned new-properties))))
|
|
||||||
|
|
||||||
|
|
||||||
(defn properties-map-to-string
|
(defn properties-map-to-string
|
||||||
|
@ -147,3 +159,75 @@
|
||||||
(when (= (:name prop) name)
|
(when (= (:name prop) name)
|
||||||
idx))
|
idx))
|
||||||
(map-indexed vector props)))
|
(map-indexed vector props)))
|
||||||
|
|
||||||
|
(defn remove-prefix
|
||||||
|
"Removes the given prefix (with or without a trailing ' / ') from the beginning of the name"
|
||||||
|
[name prefix]
|
||||||
|
(let [long-name (str prefix " / ")]
|
||||||
|
(cond
|
||||||
|
(str/starts-with? name long-name)
|
||||||
|
(subs name (count long-name))
|
||||||
|
|
||||||
|
(str/starts-with? name prefix)
|
||||||
|
(subs name (count prefix))
|
||||||
|
|
||||||
|
:else
|
||||||
|
name)))
|
||||||
|
|
||||||
|
(def ^:private xf:map-name
|
||||||
|
(map :name))
|
||||||
|
|
||||||
|
(defn- matching-indices
|
||||||
|
[props1 props2]
|
||||||
|
(let [names-in-p2 (into #{} xf:map-name props2)
|
||||||
|
xform (comp
|
||||||
|
(map-indexed (fn [index {:keys [name]}]
|
||||||
|
(when (contains? names-in-p2 name)
|
||||||
|
index)))
|
||||||
|
(filter some?))]
|
||||||
|
(into #{} xform props1)))
|
||||||
|
|
||||||
|
(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]
|
||||||
|
(some (fn [[idx item]]
|
||||||
|
(when (= (:name item) name)
|
||||||
|
idx))
|
||||||
|
(map-indexed vector props)))
|
||||||
|
|
||||||
|
(defn- next-valid-position
|
||||||
|
"Returns the first non-negative integer not present in the used-pos set."
|
||||||
|
[used-pos]
|
||||||
|
(loop [p 0]
|
||||||
|
(if (contains? used-pos p)
|
||||||
|
(recur (inc p))
|
||||||
|
p)))
|
||||||
|
|
||||||
|
(defn- find-position
|
||||||
|
"Returns the index of the property with the given name in `props`,
|
||||||
|
or the next available index not in `used-pos` if not found."
|
||||||
|
[name props used-pos]
|
||||||
|
(or (find-index-by-name name props)
|
||||||
|
(next-valid-position used-pos)))
|
||||||
|
|
||||||
|
(defn merge-properties
|
||||||
|
"Merges props2 into props1 with the following rules:
|
||||||
|
- For each property p2 in props2:
|
||||||
|
- 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
|
||||||
|
- 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)]
|
||||||
|
(-> (reduce
|
||||||
|
(fn [{:keys [props used-pos]} prop]
|
||||||
|
(let [pos (find-position (:name prop) props 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)))
|
|
@ -419,7 +419,7 @@ test("User cut paste a variant into another container", async ({ page }) => {
|
||||||
|
|
||||||
const variant3 = await workspacePage.layers
|
const variant3 = await workspacePage.layers
|
||||||
.getByTestId("layer-row")
|
.getByTestId("layer-row")
|
||||||
.filter({ has: workspacePage.page.getByText("rectangle, Value 1") })
|
.filter({ has: workspacePage.page.getByText("Value 1, rectangle") })
|
||||||
.filter({ has: workspacePage.page.locator(".icon-variant") })
|
.filter({ has: workspacePage.page.locator(".icon-variant") })
|
||||||
.first();
|
.first();
|
||||||
|
|
||||||
|
|
|
@ -403,6 +403,7 @@
|
||||||
|
|
||||||
|
|
||||||
(defn rename-variant
|
(defn rename-variant
|
||||||
|
"Rename the variant container and all components belonging to this variant"
|
||||||
[variant-id name]
|
[variant-id name]
|
||||||
(ptk/reify ::rename-variant
|
(ptk/reify ::rename-variant
|
||||||
|
|
||||||
|
@ -426,6 +427,8 @@
|
||||||
|
|
||||||
|
|
||||||
(defn rename-comp-or-variant-and-main
|
(defn rename-comp-or-variant-and-main
|
||||||
|
"If the component is in a variant, rename the variant.
|
||||||
|
If it is not, rename the component and its main"
|
||||||
[component-id name]
|
[component-id name]
|
||||||
(ptk/reify ::rename-comp-or-variant-and-main
|
(ptk/reify ::rename-comp-or-variant-and-main
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue