diff --git a/backend/src/app/rpc/mutations/files.clj b/backend/src/app/rpc/mutations/files.clj index b3ee8cb3b..ccdacf86a 100644 --- a/backend/src/app/rpc/mutations/files.clj +++ b/backend/src/app/rpc/mutations/files.clj @@ -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 diff --git a/common/src/app/common/pages/migrations.cljc b/common/src/app/common/pages/migrations.cljc index 601671bfd..cb186933f 100644 --- a/common/src/app/common/pages/migrations.cljc +++ b/common/src/app/common/pages/migrations.cljc @@ -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. diff --git a/common/src/app/common/types/component.cljc b/common/src/app/common/types/component.cljc new file mode 100644 index 000000000..5dbe23865 --- /dev/null +++ b/common/src/app/common/types/component.cljc @@ -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)))) + diff --git a/common/src/app/common/types/components_list.cljc b/common/src/app/common/types/components_list.cljc index 6f5643e67..6fb2b0872 100644 --- a/common/src/app/common/types/components_list.cljc +++ b/common/src/app/common/types/components_list.cljc @@ -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)) + diff --git a/common/src/app/common/types/container.cljc b/common/src/app/common/types/container.cljc index 7f7e8d985..63a598504 100644 --- a/common/src/app/common/types/container.cljc +++ b/common/src/app/common/types/container.cljc @@ -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 diff --git a/common/src/app/common/types/file.cljc b/common/src/app/common/types/file.cljc index b999f3ccd..e9543453d 100644 --- a/common/src/app/common/types/file.cljc +++ b/common/src/app/common/types/file.cljc @@ -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 diff --git a/common/src/app/common/types/shape_tree.cljc b/common/src/app/common/types/shape_tree.cljc index c7294699a..b03054e24 100644 --- a/common/src/app/common/types/shape_tree.cljc +++ b/common/src/app/common/types/shape_tree.cljc @@ -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})))) diff --git a/common/test/app/common/types/file_test.cljc b/common/test/app/common/types/file_test.cljc index 0049732ab..76764c69d 100644 --- a/common/test/app/common/types/file_test.cljc +++ b/common/test/app/common/types/file_test.cljc @@ -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))))) diff --git a/frontend/src/app/main/data/workspace/shapes.cljs b/frontend/src/app/main/data/workspace/shapes.cljs index 237ce6029..880db210c 100644 --- a/frontend/src/app/main/data/workspace/shapes.cljs +++ b/frontend/src/app/main/data/workspace/shapes.cljs @@ -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)))))) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/component.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/component.cljs index 3e19ec5f9..0e6debc1a 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/component.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/component.cljs @@ -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])