🎉 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 ;; --- Mutation: Set File shared
(declare set-file-shared) (declare set-file-shared)
(declare unlink-files)
(declare absorb-library)
(s/def ::set-file-shared (s/def ::set-file-shared
(s/keys :req-un [::profile-id ::id ::is-shared])) (s/keys :req-un [::profile-id ::id ::is-shared]))
(sv/defmethod ::set-file-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] (db/with-atomic [conn pool]
(files/check-edition-permissions! conn profile-id id) (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))) (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 (defn- set-file-shared
[conn {:keys [id is-shared] :as params}] [conn {:keys [id is-shared] :as params}]
(db/update! conn :file (db/update! conn :file
@ -138,6 +151,7 @@
[{:keys [pool] :as cfg} {:keys [id profile-id] :as params}] [{:keys [pool] :as cfg} {:keys [id profile-id] :as params}]
(db/with-atomic [conn pool] (db/with-atomic [conn pool]
(files/check-edition-permissions! conn profile-id id) (files/check-edition-permissions! conn profile-id id)
(absorb-library conn params)
(mark-file-deleted conn params))) (mark-file-deleted conn params)))
(defn mark-file-deleted (defn mark-file-deleted
@ -147,6 +161,35 @@
{:id id}) {:id id})
nil) 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 ;; --- Mutation: Link file to library

View file

@ -15,7 +15,9 @@
[app.common.math :as mth] [app.common.math :as mth]
[app.common.pages :as cp] [app.common.pages :as cp]
[app.common.pages.helpers :as cph] [app.common.pages.helpers :as cph]
[app.common.types.components-list :as ctkl]
[app.common.types.container :as ctn] [app.common.types.container :as ctn]
[app.common.types.file :as ctf]
[app.common.types.page :as ctp] [app.common.types.page :as ctp]
[app.common.types.pages-list :as ctpl] [app.common.types.pages-list :as ctpl]
[app.common.types.shape :as cts] [app.common.types.shape :as cts]
@ -439,70 +441,65 @@
(defmethod migrate 20 (defmethod migrate 20
[data] [data]
(let [page-id (uuid/next) (let [components (ctkl/components-seq data)]
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)))))))]
(if (empty? components) (if (empty? components)
data data
(-> data (let [grid-gap 50
(add-library-page)
(add-instance-grid components))))) [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 ;; TODO: pending to do a migration for delete already not used fill
;; and stroke props. This should be done for >1.14.x version. ;; 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] [file-data component-id]
(get-in file-data [:components 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] [container]
(vals (:objects container))) (vals (:objects container)))
(defn update-shape
[container shape-id f]
(update-in container [:objects shape-id] f))
(defn make-component-shape (defn make-component-shape
"Clone the shape and all children. Generate new ids and detach "Clone the shape and all children. Generate new ids and detach
from parent and frame. Update the original shapes to have links from parent and frame. Update the original shapes to have links

View file

@ -6,18 +6,22 @@
(ns app.common.types.file (ns app.common.types.file
(:require (:require
[app.common.data :as d] [app.common.data :as d]
[app.common.pages.common :refer [file-version]] [app.common.geom.point :as gpt]
[app.common.pages.helpers :as cph] [app.common.geom.shapes :as gsh]
[app.common.spec :as us] [app.common.pages.common :refer [file-version]]
[app.common.types.color :as ctc] [app.common.pages.helpers :as cph]
[app.common.types.components-list :as ctkl] [app.common.spec :as us]
[app.common.types.container :as ctn] [app.common.types.color :as ctc]
[app.common.types.page :as ctp] [app.common.types.component :as ctk]
[app.common.types.pages-list :as ctpl] [app.common.types.components-list :as ctkl]
[app.common.uuid :as uuid] [app.common.types.container :as ctn]
[clojure.spec.alpha :as s] [app.common.types.page :as ctp]
[cuerdas.core :as str])) [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 ;; Specs
@ -97,48 +101,270 @@
(concat (map #(ctn/make-container % :page) (ctpl/pages-seq file-data)) (concat (map #(ctn/make-container % :page) (ctpl/pages-seq file-data))
(map #(ctn/make-container % :component) (ctkl/components-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 (defn absorb-assets
"Find all assets of a library that are used in the file, and "Find all assets of a library that are used in the file, and
move them to the file local library." move them to the file local library."
[file-data library-data] [file-data library-data]
(let [library-page-id (uuid/next) (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]...]]...]
add-library-page used-components ; A vector of pair [component instances], where instances is non-empty
(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
(mapcat (fn [component] (mapcat (fn [component]
(let [instances (find-instances file-data component)] (let [instances (find-instances file-data component)]
(when instances (when (d/not-empty? instances)
[[component instances]]))) [[component instances]])))
(ctkl/components-seq library-data))] (ctkl/components-seq library-data))]
(if (empty? used-components) (if (empty? used-components)
file-data file-data
(as-> file-data $ (let [; Search for the library page. If not exists, create it.
(add-library-page $) [file-data page-id start-pos]
(reduce absorb-component (get-or-add-library-page file-data)
$
used-components))))) 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 ;; Debug helpers

View file

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

View file

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

View file

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

View file

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