0
Fork 0
mirror of https://github.com/penpot/penpot.git synced 2025-03-27 23:21:47 -05:00

🎉 Absorb components when deleting or unpublishing a library

This commit is contained in:
Andrés Moya 2022-06-29 15:23:29 +02:00
parent 54e0071c9c
commit 7da159d52a
10 changed files with 436 additions and 149 deletions

View file

@ -111,16 +111,29 @@
;; --- Mutation: Set File shared
(declare set-file-shared)
(declare unlink-files)
(declare absorb-library)
(s/def ::set-file-shared
(s/keys :req-un [::profile-id ::id ::is-shared]))
(sv/defmethod ::set-file-shared
[{:keys [pool] :as cfg} {:keys [id profile-id] :as params}]
[{:keys [pool] :as cfg} {:keys [id profile-id is-shared] :as params}]
(db/with-atomic [conn pool]
(files/check-edition-permissions! conn profile-id id)
(when-not is-shared
(absorb-library conn params)
(unlink-files conn params))
(set-file-shared conn params)))
(def sql:unlink-files
"delete from file_library_rel
where library_file_id = ?")
(defn- unlink-files
[conn {:keys [id] :as params}]
(db/exec-one! conn [sql:unlink-files id]))
(defn- set-file-shared
[conn {:keys [id is-shared] :as params}]
(db/update! conn :file
@ -138,6 +151,7 @@
[{:keys [pool] :as cfg} {:keys [id profile-id] :as params}]
(db/with-atomic [conn pool]
(files/check-edition-permissions! conn profile-id id)
(absorb-library conn params)
(mark-file-deleted conn params)))
(defn mark-file-deleted
@ -147,6 +161,35 @@
{:id id})
nil)
(def sql:find-files
"select file_id
from file_library_rel
where library_file_id=?")
(defn absorb-library
"Find all files using a shared library, and absorb all library assets
into the file local libraries"
[conn {:keys [id] :as params}]
(let [library (->> (db/get-by-id conn :file id)
(files/decode-row)
(pmg/migrate-file))]
(when (:is-shared library)
(let [process-file
(fn [row]
(let [ts (dt/now)
file (->> (db/get-by-id conn :file (:file-id row))
(files/decode-row)
(pmg/migrate-file))
updated-data (ctf/absorb-assets (:data file) (:data library))]
(db/update! conn :file
{:revn (inc (:revn file))
:data (blob/encode updated-data)
:modified-at ts}
{:id (:id file)})))]
(dorun (->> (db/exec! conn [sql:find-files id])
(map process-file)))))))
;; --- Mutation: Link file to library

View file

@ -15,7 +15,9 @@
[app.common.math :as mth]
[app.common.pages :as cp]
[app.common.pages.helpers :as cph]
[app.common.types.components-list :as ctkl]
[app.common.types.container :as ctn]
[app.common.types.file :as ctf]
[app.common.types.page :as ctp]
[app.common.types.pages-list :as ctpl]
[app.common.types.shape :as cts]
@ -439,70 +441,65 @@
(defmethod migrate 20
[data]
(let [page-id (uuid/next)
components (->> (:components data)
vals
(sort-by :name))
add-library-page
(fn [data]
(let [page (ctp/make-empty-page page-id "Library page")]
(-> data
(ctpl/add-page page))))
add-main-instance
(fn [data component position]
(let [page (ctpl/get-page data page-id)
[new-shape new-shapes]
(ctn/instantiate-component page
component
(:id data)
position)
add-shape
(fn [data shape]
(update-in data [:pages-index page-id]
#(ctst/add-shape (:id shape)
shape
%
(:frame-id shape)
(:parent-id shape)
nil ; <- As shapes are ordered, we can safely add each
true))) ; one at the end of the parent's children list.
update-component
(fn [component]
(assoc component
:main-instance-id (:id new-shape)
:main-instance-page page-id))]
(as-> data $
(reduce add-shape $ new-shapes)
(update-in $ [:components (:id component)] update-component))))
add-instance-grid
(fn [data components]
(let [position-seq (ctst/generate-shape-grid
(map cph/get-component-root components)
50)]
(loop [data data
components-seq (seq components)
position-seq position-seq]
(let [component (first components-seq)
position (first position-seq)]
(if (nil? component)
data
(recur (add-main-instance data component position)
(rest components-seq)
(rest position-seq)))))))]
(let [components (ctkl/components-seq data)]
(if (empty? components)
data
(-> data
(add-library-page)
(add-instance-grid components)))))
(let [grid-gap 50
[data page-id start-pos]
(ctf/get-or-add-library-page data grid-gap)
add-main-instance
(fn [data component position]
(let [page (ctpl/get-page data page-id)
[new-shape new-shapes]
(ctn/instantiate-component page
component
(:id data)
position)
add-shapes
(fn [page]
(reduce (fn [page shape]
(ctst/add-shape (:id shape)
shape
page
(:frame-id shape)
(:parent-id shape)
nil ; <- As shapes are ordered, we can safely add each
true)) ; one at the end of the parent's children list.
page
new-shapes))
update-component
(fn [component]
(assoc component
:main-instance-id (:id new-shape)
:main-instance-page page-id))]
(-> data
(ctpl/update-page page-id add-shapes)
(ctkl/update-component (:id component) update-component))))
add-instance-grid
(fn [data components]
(let [position-seq (ctst/generate-shape-grid
(map cph/get-component-root components)
start-pos
grid-gap)]
(loop [data data
components-seq (seq components)
position-seq position-seq]
(let [component (first components-seq)
position (first position-seq)]
(if (nil? component)
data
(recur (add-main-instance data component position)
(rest components-seq)
(rest position-seq)))))))]
(add-instance-grid data (sort-by :name components))))))
;; TODO: pending to do a migration for delete already not used fill
;; and stroke props. This should be done for >1.14.x version.

View file

@ -0,0 +1,13 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) UXBOX Labs SL
(ns app.common.types.component)
(defn instance-of?
[shape component]
(and (some? (:component-id shape))
(= (:component-id shape) (:id component))))

View file

@ -26,3 +26,7 @@
[file-data component-id]
(get-in file-data [:components component-id]))
(defn update-component
[file-data component-id f]
(update-in file-data [:components component-id] f))

View file

@ -56,6 +56,10 @@
[container]
(vals (:objects container)))
(defn update-shape
[container shape-id f]
(update-in container [:objects shape-id] f))
(defn make-component-shape
"Clone the shape and all children. Generate new ids and detach
from parent and frame. Update the original shapes to have links

View file

@ -6,18 +6,22 @@
(ns app.common.types.file
(:require
[app.common.data :as d]
[app.common.pages.common :refer [file-version]]
[app.common.pages.helpers :as cph]
[app.common.spec :as us]
[app.common.types.color :as ctc]
[app.common.types.components-list :as ctkl]
[app.common.types.container :as ctn]
[app.common.types.page :as ctp]
[app.common.types.pages-list :as ctpl]
[app.common.uuid :as uuid]
[clojure.spec.alpha :as s]
[cuerdas.core :as str]))
[app.common.data :as d]
[app.common.geom.point :as gpt]
[app.common.geom.shapes :as gsh]
[app.common.pages.common :refer [file-version]]
[app.common.pages.helpers :as cph]
[app.common.spec :as us]
[app.common.types.color :as ctc]
[app.common.types.component :as ctk]
[app.common.types.components-list :as ctkl]
[app.common.types.container :as ctn]
[app.common.types.page :as ctp]
[app.common.types.pages-list :as ctpl]
[app.common.types.shape-tree :as ctst]
[app.common.uuid :as uuid]
[clojure.spec.alpha :as s]
[cuerdas.core :as str]))
;; Specs
@ -97,48 +101,270 @@
(concat (map #(ctn/make-container % :page) (ctpl/pages-seq file-data))
(map #(ctn/make-container % :component) (ctkl/components-seq file-data))))
(defn update-container
"Update a container inside the file, it can be a page or a component"
[file-data container f]
(if (ctn/page? container)
(ctpl/update-page file-data (:id container) f)
(ctkl/update-component file-data (:id container) f)))
(defn find-instances
"Find all uses of a component in a file (may be in pages or in the components
of the local library).
Returns a vector [[container shapes] [container shapes]...]"
[file-data component]
(let [find-instances-in-container
(fn [container component]
(let [instances (filter #(ctk/instance-of? % component) (ctn/shapes-seq container))]
(when (d/not-empty? instances)
[[container instances]])))]
(mapcat #(find-instances-in-container % component) (containers-seq file-data))))
(defn get-or-add-library-page
[file-data grid-gap]
"If exists a page named 'Library page', get the id and calculate the position to start
adding new components. If not, create it and start at (0, 0)."
(let [library-page (d/seek #(= (:name %) "Library page") (ctpl/pages-seq file-data))]
(if (some? library-page)
(let [compare-pos (fn [pos shape]
(let [bounds (gsh/bounding-box shape)]
(gpt/point (min (:x pos) (get bounds :x 0))
(max (:y pos) (+ (get bounds :y 0)
(get bounds :height 0)
grid-gap)))))
position (reduce compare-pos
(gpt/point 0 0)
(ctn/shapes-seq library-page))]
[file-data (:id library-page) position])
(let [library-page (ctp/make-empty-page (uuid/next) "Library page")]
[(ctpl/add-page file-data library-page) (:id library-page) (gpt/point 0 0)]))))
(defn- absorb-components
[file-data library-data used-components]
(let [grid-gap 50
; Search for the library page. If not exists, create it.
[file-data page-id start-pos]
(get-or-add-library-page file-data grid-gap)
absorb-component
(fn [file-data [component instances] position]
(let [page (ctpl/get-page file-data page-id)
; Make a new main instance for the component
[main-instance-shape main-instance-shapes]
(ctn/instantiate-component page
component
(:id file-data)
position)
; Add all shapes of the main instance to the library page
add-main-instance-shapes
(fn [page]
(reduce (fn [page shape]
(ctst/add-shape (:id shape)
shape
page
(:frame-id shape)
(:parent-id shape)
nil ; <- As shapes are ordered, we can safely add each
true)) ; one at the end of the parent's children list.
page
main-instance-shapes))
; Copy the component in the file local library
copy-component
(fn [file-data]
(ctkl/add-component file-data
(:id component)
(:name component)
(:path component)
(:id main-instance-shape)
page-id
(vals (:objects component))))
; Change all existing instances to point to the local file
remap-instances
(fn [file-data [container shapes]]
(let [remap-instance #(assoc % :component-file (:id file-data))]
(update-container file-data
container
#(reduce (fn [container shape]
(ctn/update-shape container
(:id shape)
remap-instance))
%
shapes))))]
(as-> file-data $
(ctpl/update-page $ page-id add-main-instance-shapes)
(copy-component $)
(reduce remap-instances $ instances))))
; Absorb all used components into the local library. Position
; the main instances in a grid in the library page.
add-component-grid
(fn [data used-components]
(let [position-seq (ctst/generate-shape-grid
(map #(ctk/get-component-root (first %)) used-components)
start-pos
grid-gap)]
(loop [data data
components-seq (seq used-components)
position-seq position-seq]
(let [used-component (first components-seq)
position (first position-seq)]
(if (nil? used-component)
data
(recur (absorb-component data used-component position)
(rest components-seq)
(rest position-seq)))))))]
(add-component-grid file-data (sort-by #(:name (first %)) used-components))))
(defn- absorb-colors
[file-data library-data used-colors]
(let [absorb-color
(fn [file-data [color usages]]
(let [remap-shape #(ctc/remap-colors % (:id file-data) color)
remap-shapes
(fn [file-data [container shapes]]
(update-container file-data
container
#(reduce (fn [container shape]
(ctn/update-shape container
(:id shape)
remap-shape))
%
shapes)))]
(as-> file-data $
(ctcl/add-color $ color)
(reduce remap-shapes $ usages))))]
(reduce absorb-color
file-data
used-colors)))
(defn- absorb-typographies
[file-data library-data used-typographies]
(let [absorb-typography
(fn [file-data [typography usages]]
(let [remap-shape #(cty/remap-typographies % (:id file-data) typography)
remap-shapes
(fn [file-data [container shapes]]
(update-container file-data
container
#(reduce (fn [container shape]
(ctn/update-shape container
(:id shape)
remap-shape))
%
shapes)))]
(as-> file-data $
(ctyl/add-typography $ typography)
(reduce remap-shapes $ usages))))]
(reduce absorb-typography
file-data
used-typographies)))
(defn absorb-assets
"Find all assets of a library that are used in the file, and
move them to the file local library."
[file-data library-data]
(let [library-page-id (uuid/next)
add-library-page
(fn [file-data]
(let [page (ctp/make-empty-page library-page-id "Library page")]
(-> file-data
(ctpl/add-page page))))
find-instances-in-container
(fn [container component]
(let [instances (filter #(= (:component-id %) (:id component))
(ctn/shapes-seq container))]
(when (d/not-empty? instances)
[[container instances]])))
find-instances
(fn [file-data component]
(mapcat #(find-instances-in-container % component) (containers-seq file-data)))
absorb-component
(fn [file-data _component]
;; TODO: complete this
file-data)
used-components
(let [; Build a list of all components in the library used in the file
; The list is in the form [[component [[container shapes] [container shapes]...]]...]
used-components ; A vector of pair [component instances], where instances is non-empty
(mapcat (fn [component]
(let [instances (find-instances file-data component)]
(when instances
(when (d/not-empty? instances)
[[component instances]])))
(ctkl/components-seq library-data))]
(if (empty? used-components)
file-data
(as-> file-data $
(add-library-page $)
(reduce absorb-component
$
used-components)))))
(let [; Search for the library page. If not exists, create it.
[file-data page-id start-pos]
(get-or-add-library-page file-data)
absorb-component
(fn [file-data [component instances] position]
(let [page (ctpl/get-page file-data page-id)
; Make a new main instance for the component
[main-instance-shape main-instance-shapes]
(ctn/instantiate-component page
component
(:id file-data)
position)
; Add all shapes of the main instance to the library page
add-main-instance-shapes
(fn [page]
(reduce (fn [page shape]
(ctst/add-shape (:id shape)
shape
page
(:frame-id shape)
(:parent-id shape)
nil ; <- As shapes are ordered, we can safely add each
true)) ; one at the end of the parent's children list.
page
main-instance-shapes))
; Copy the component in the file local library
copy-component
(fn [file-data]
(ctkl/add-component file-data
(:id component)
(:name component)
(:path component)
(:id main-instance-shape)
page-id
(vals (:objects component))))
; Change all existing instances to point to the local file
redirect-instances
(fn [file-data [container shapes]]
(let [redirect-instance #(assoc % :component-file (:id file-data))]
(update-container file-data
container
#(reduce (fn [container shape]
(ctn/update-shape container
(:id shape)
redirect-instance))
%
shapes))))]
(as-> file-data $
(ctpl/update-page $ page-id add-main-instance-shapes)
(copy-component $)
(reduce redirect-instances $ instances))))
; Absorb all used components into the local library. Position
; the main instances in a grid in the library page.
add-component-grid
(fn [data used-components]
(let [position-seq (ctst/generate-shape-grid
(map #(cph/get-component-root (first %)) used-components)
start-pos
50)]
(loop [data data
components-seq (seq used-components)
position-seq position-seq]
(let [used-component (first components-seq)
position (first position-seq)]
(if (nil? used-component)
data
(recur (absorb-component data used-component position)
(rest components-seq)
(rest position-seq)))))))]
(add-component-grid file-data (sort-by #(:name (first %)) used-components))))))
;; Debug helpers

View file

@ -326,7 +326,7 @@
(defn generate-shape-grid
"Generate a sequence of positions that lays out the list of
shapes in a grid of equal-sized rows and columns."
[shapes gap]
[shapes start-pos gap]
(let [shapes-bounds (map gsh/bounding-box shapes)
grid-size (mth/ceil (mth/sqrt (count shapes)))
@ -339,11 +339,12 @@
(let [counter (inc (:counter (meta position)))
row (quot counter grid-size)
column (mod counter grid-size)
new-pos (gpt/point (* column column-size)
(* row row-size))]
new-pos (gpt/add start-pos
(gpt/point (* column column-size)
(* row row-size)))]
(with-meta new-pos
{:counter counter})))]
(iterate next-pos
(with-meta (gpt/point 0 0)
(with-meta start-pos
{:counter 0}))))

View file

@ -54,25 +54,23 @@
file
#(ctf/absorb-assets % (:data library)))]
(println "\n===== library")
(ctf/dump-tree (:data library)
library-page-id
{}
true)
;; (println "\n===== library")
;; (ctf/dump-tree (:data library)
;; library-page-id
;; {}
;; true)
(println "\n===== file")
(ctf/dump-tree (:data file)
file-page-id
{library-id {:id library-id
:name "Library 1"
:data library}}
true)
;; (println "\n===== file")
;; (ctf/dump-tree (:data file)
;; file-page-id
;; {library-id library}
;; true)
(println "\n===== absorbed file")
(ctf/dump-tree (:data absorbed-file)
file-page-id
{}
true)
;; (println "\n===== absorbed file")
;; (ctf/dump-tree (:data absorbed-file)
;; file-page-id
;; {}
;; true)
(t/is (= library-id (:id library)))
(t/is (= file-id (:id absorbed-file)))))

View file

@ -13,10 +13,10 @@
[app.common.pages.changes-builder :as pcb]
[app.common.pages.helpers :as cph]
[app.common.spec :as us]
[app.common.types.page :as csp]
[app.common.types.shape :as spec.shape]
[app.common.types.shape.interactions :as csi]
[app.common.types.shape-tree :as ctt]
[app.common.types.page :as ctp]
[app.common.types.shape :as cts]
[app.common.types.shape.interactions :as ctsi]
[app.common.types.shape-tree :as ctst]
[app.common.uuid :as uuid]
[app.main.data.workspace.changes :as dch]
[app.main.data.workspace.edition :as dwe]
@ -28,14 +28,14 @@
[cljs.spec.alpha :as s]
[potok.core :as ptk]))
(s/def ::shape-attrs ::spec.shape/shape-attrs)
(s/def ::shape-attrs ::cts/shape-attrs)
(defn get-shape-layer-position
[objects selected attrs]
;; Calculate the frame over which we're drawing
(let [position @ms/mouse-position
frame-id (:frame-id attrs (cph/frame-id-by-position objects position))
frame-id (:frame-id attrs (ctst/frame-id-by-position objects position))
shape (when-not (empty? selected)
(cph/get-base-shape objects selected))]
@ -52,8 +52,8 @@
(defn make-new-shape
[attrs objects selected]
(let [default-attrs (if (= :frame (:type attrs))
cp/default-frame-attrs
cp/default-shape-attrs)
cts/default-frame-attrs
cts/default-shape-attrs)
selected-non-frames
(into #{} (comp (map (d/getf objects))
@ -117,7 +117,7 @@
to-move-shapes
(into []
(map (d/getf objects))
(reverse (cph/sort-z-index objects shapes)))
(reverse (ctst/sort-z-index objects shapes)))
changes
(when (d/not-empty? to-move-shapes)
@ -289,10 +289,10 @@
y (:y data (- vbc-y (/ height 2)))
page-id (:current-page-id state)
frame-id (-> (wsh/lookup-page-objects state page-id)
(cph/frame-id-by-position {:x frame-x :y frame-y}))
shape (-> (cp/make-minimal-shape type)
(ctst/frame-id-by-position {:x frame-x :y frame-y}))
shape (-> (cts/make-minimal-shape type)
(merge data)
(merge {:x x :y y})
(assoc :frame-id frame-id)
(cp/setup-rect-selrect))]
(cts/setup-rect-selrect))]
(rx/of (add-shape shape))))))

View file

@ -6,16 +6,17 @@
(ns app.main.ui.workspace.sidebar.options.menus.component
(:require
[app.main.data.modal :as modal]
[app.main.data.workspace :as dw]
[app.main.data.workspace.libraries :as dwl]
[app.main.store :as st]
[app.main.ui.components.context-menu :refer [context-menu]]
[app.main.ui.context :as ctx]
[app.main.ui.icons :as i]
[app.util.dom :as dom]
[app.util.i18n :as i18n :refer [tr]]
[rumext.alpha :as mf]))
[app.common.pages.helpers :as cph]
[app.main.data.modal :as modal]
[app.main.data.workspace :as dw]
[app.main.data.workspace.libraries :as dwl]
[app.main.store :as st]
[app.main.ui.components.context-menu :refer [context-menu]]
[app.main.ui.context :as ctx]
[app.main.ui.icons :as i]
[app.util.dom :as dom]
[app.util.i18n :as i18n :refer [tr]]
[rumext.alpha :as mf]))
(def component-attrs [:component-id :component-file :shape-ref])