diff --git a/backend/src/app/rpc/mutations/files.clj b/backend/src/app/rpc/mutations/files.clj index dba09a40a..ff45fb6c6 100644 --- a/backend/src/app/rpc/mutations/files.clj +++ b/backend/src/app/rpc/mutations/files.clj @@ -11,6 +11,7 @@ [app.common.pages :as cp] [app.common.pages.migrations :as pmg] [app.common.spec :as us] + [app.common.types.file :as ctf] [app.common.uuid :as uuid] [app.config :as cf] [app.db :as db] @@ -45,7 +46,7 @@ (s/def ::is-shared ::us/boolean) (s/def ::create-file (s/keys :req-un [::profile-id ::name ::project-id] - :opt-un [::id ::is-shared])) + :opt-un [::id ::is-shared ::components-v2])) (sv/defmethod ::create-file [{:keys [pool] :as cfg} {:keys [profile-id project-id] :as params}] @@ -65,11 +66,12 @@ (defn create-file [conn {:keys [id name project-id is-shared data revn - modified-at deleted-at ignore-sync-until] + modified-at deleted-at ignore-sync-until + components-v2] :or {is-shared false revn 0} :as params}] (let [id (or id (:id data) (uuid/next)) - data (or data (cp/make-file-data id)) + data (or data (ctf/make-file-data id components-v2)) file (db/insert! conn :file (d/without-nils {:id id @@ -110,16 +112,25 @@ ;; --- 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))) +(defn- unlink-files + [conn {:keys [id] :as params}] + (db/delete! conn :file-library-rel {:library-file-id id})) + (defn- set-file-shared [conn {:keys [id is-shared] :as params}] (db/update! conn :file @@ -137,6 +148,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 @@ -146,6 +158,29 @@ {:id id}) nil) +(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)})))] + + (run! process-file (db/query conn :file-library-rel {:library-file-id id})))))) ;; --- Mutation: Link file to library @@ -273,10 +308,11 @@ (s/def ::session-id ::us/uuid) (s/def ::revn ::us/integer) +(s/def ::components-v2 ::us/boolean) (s/def ::update-file (s/and (s/keys :req-un [::id ::session-id ::profile-id ::revn] - :opt-un [::changes ::changes-with-metadata]) + :opt-un [::changes ::changes-with-metadata ::components-v2]) (fn [o] (or (contains? o :changes) (contains? o :changes-with-metadata))))) @@ -313,7 +349,8 @@ (simpl/del-object backend file)))) (defn- update-file - [{:keys [conn metrics] :as cfg} {:keys [file changes changes-with-metadata session-id profile-id] :as params}] + [{:keys [conn metrics] :as cfg} + {:keys [file changes changes-with-metadata session-id profile-id components-v2] :as params}] (when (> (:revn params) (:revn file)) @@ -338,12 +375,18 @@ (update :data (fn [data] ;; Trace the length of bytes of processed data (mtx/run! metrics {:id :update-file-bytes-processed :inc (alength data)}) - (-> data - (blob/decode) - (assoc :id (:id file)) - (pmg/migrate-data) - (cp/process-changes changes) - (blob/encode)))))] + (cond-> data + :always + (-> (blob/decode) + (assoc :id (:id file)) + (pmg/migrate-data)) + + components-v2 + (ctf/migrate-to-components-v2) + + :always + (-> (cp/process-changes changes) + (blob/encode))))))] ;; Insert change to the xlog (db/insert! conn :file-change {:id (uuid/next) diff --git a/backend/src/app/rpc/queries/files.clj b/backend/src/app/rpc/queries/files.clj index 18ec92848..838f35d31 100644 --- a/backend/src/app/rpc/queries/files.clj +++ b/backend/src/app/rpc/queries/files.clj @@ -13,6 +13,8 @@ [app.common.pages.helpers :as cph] [app.common.pages.migrations :as pmg] [app.common.spec :as us] + [app.common.types.file :as ctf] + [app.common.types.shape-tree :as ctt] [app.db :as db] [app.db.sql :as sql] [app.rpc.helpers :as rpch] @@ -26,7 +28,6 @@ [cuerdas.core :as str])) (declare decode-row) -(declare decode-row-xf) ;; --- Helpers & Specs @@ -38,6 +39,7 @@ (s/def ::profile-id ::us/uuid) (s/def ::team-id ::us/uuid) (s/def ::search-term ::us/string) +(s/def ::components-v2 ::us/boolean) ;; --- Query: File Permissions @@ -122,8 +124,7 @@ (defn check-comment-permissions! [conn profile-id file-id share-id] (let [can-read (has-read-permissions? conn profile-id file-id) - can-comment (has-comment-permissions? conn profile-id file-id share-id) - ] + can-comment (has-comment-permissions? conn profile-id file-id share-id)] (when-not (or can-read can-comment) (ex/raise :type :not-found :code :object-not-found @@ -226,20 +227,29 @@ (d/index-by :object-id :data)))))) (defn retrieve-file - [{:keys [pool] :as cfg} id] - (->> (db/get-by-id pool :file id) - (decode-row) - (pmg/migrate-file))) + [{:keys [pool] :as cfg} id components-v2] + (let [file (->> (db/get-by-id pool :file id) + (decode-row) + (pmg/migrate-file))] + + (if components-v2 + (update file :data ctf/migrate-to-components-v2) + (if (get-in file [:data :options :components-v2]) + (ex/raise :type :restriction + :code :feature-disabled + :hint "tried to open a components-v2 file with feature disabled") + file)))) (s/def ::file - (s/keys :req-un [::profile-id ::id])) + (s/keys :req-un [::profile-id ::id] + :opt-un [::components-v2])) (sv/defmethod ::file "Retrieve a file by its ID. Only authenticated users." - [{:keys [pool] :as cfg} {:keys [profile-id id] :as params}] + [{:keys [pool] :as cfg} {:keys [profile-id id components-v2] :as params}] (let [perms (get-permissions pool profile-id id)] (check-read-permissions! perms) - (let [file (retrieve-file cfg id) + (let [file (retrieve-file cfg id components-v2) thumbs (retrieve-object-thumbnails cfg id)] (-> file (assoc :thumbnails thumbs) @@ -268,7 +278,7 @@ (s/def ::page (s/and (s/keys :req-un [::profile-id ::file-id] - :opt-un [::page-id ::object-id]) + :opt-un [::page-id ::object-id ::components-v2]) (fn [obj] (if (contains? obj :object-id) (contains? obj :page-id) @@ -284,9 +294,9 @@ mandatory. Mainly used for rendering purposes." - [{:keys [pool] :as cfg} {:keys [profile-id file-id page-id object-id] :as props}] + [{:keys [pool] :as cfg} {:keys [profile-id file-id page-id object-id components-v2] :as props}] (check-read-permissions! pool profile-id file-id) - (let [file (retrieve-file cfg file-id) + (let [file (retrieve-file cfg file-id components-v2) page-id (or page-id (-> file :data :pages first)) page (get-in file [:data :pages-index page-id])] @@ -304,7 +314,7 @@ (get-thumbnail-frame [data] (d/seek :use-for-thumbnail? (for [page (-> data :pages-index vals) - frame (-> page :objects cph/get-frames)] + frame (-> page :objects ctt/get-frames)] (assoc frame :page-id (:id page))))) ;; function responsible to filter objects data structure of @@ -355,7 +365,7 @@ (-> data :pages first)) page (dm/get-in data [:pages-index page-id]) - frame-ids (if (some? frame) (list frame-id) (map :id (cph/get-frames (:objects page)))) + frame-ids (if (some? frame) (list frame-id) (map :id (ctt/get-frames (:objects page)))) obj-ids (map #(str page-id %) frame-ids) thumbs (retrieve-object-thumbnails cfg id obj-ids)] @@ -373,14 +383,15 @@ (update :objects assoc-thumbnails page-id thumbs))))) (s/def ::file-data-for-thumbnail - (s/keys :req-un [::profile-id ::file-id])) + (s/keys :req-un [::profile-id ::file-id] + :opt-un [::components-v2])) (sv/defmethod ::file-data-for-thumbnail "Retrieves the data for generate the thumbnail of the file. Used mainly for render thumbnails on dashboard." - [{:keys [pool] :as cfg} {:keys [profile-id file-id] :as props}] + [{:keys [pool] :as cfg} {:keys [profile-id file-id components-v2] :as props}] (check-read-permissions! pool profile-id file-id) - (let [file (retrieve-file cfg file-id)] + (let [file (retrieve-file cfg file-id components-v2)] {:file-id file-id :revn (:revn file) :page (get-file-thumbnail-data cfg file)})) @@ -453,6 +464,24 @@ (check-read-permissions! pool profile-id file-id) (retrieve-file-libraries cfg false file-id)) + +;; --- Query: Files that use this File library + +(def ^:private sql:library-using-files + "SELECT f.id, + f.name + FROM file_library_rel AS flr + INNER JOIN file AS f ON f.id = flr.file_id + WHERE flr.library_file_id = ?;") + +(s/def ::library-using-files + (s/keys :req-un [::profile-id ::file-id])) + +(sv/defmethod ::library-using-files + [{:keys [pool] :as cfg} {:keys [profile-id file-id] :as params}] + (check-read-permissions! pool profile-id file-id) + (db/exec! pool [sql:library-using-files file-id])) + ;; --- QUERY: team-recent-files (def sql:team-recent-files @@ -522,7 +551,3 @@ (cond-> row changes (assoc :changes (blob/decode changes)) data (assoc :data (blob/decode data))))) - -(def decode-row-xf - (comp (map decode-row) - (map pmg/migrate-file))) diff --git a/backend/src/app/rpc/queries/viewer.clj b/backend/src/app/rpc/queries/viewer.clj index 681b8ef47..03d9c9342 100644 --- a/backend/src/app/rpc/queries/viewer.clj +++ b/backend/src/app/rpc/queries/viewer.clj @@ -23,8 +23,8 @@ (db/get-by-id pool :project id {:columns [:id :name :team-id]})) (defn- retrieve-bundle - [{:keys [pool] :as cfg} file-id profile-id] - (p/let [file (files/retrieve-file cfg file-id) + [{:keys [pool] :as cfg} file-id profile-id components-v2] + (p/let [file (files/retrieve-file cfg file-id components-v2) project (retrieve-project pool (:project-id file)) libs (files/retrieve-file-libraries cfg false file-id) users (comments/retrieve-file-comments-users pool file-id profile-id) @@ -47,14 +47,14 @@ (s/def ::share-id ::us/uuid) (s/def ::view-only-bundle - (s/keys :req-un [::file-id] :opt-un [::profile-id ::share-id])) + (s/keys :req-un [::file-id] :opt-un [::profile-id ::share-id ::components-v2])) (sv/defmethod ::view-only-bundle {:auth false} - [{:keys [pool] :as cfg} {:keys [profile-id file-id share-id] :as params}] + [{:keys [pool] :as cfg} {:keys [profile-id file-id share-id components-v2] :as params}] (p/let [slink (slnk/retrieve-share-link pool file-id share-id) perms (files/get-permissions pool profile-id file-id share-id) thumbs (files/retrieve-object-thumbnails cfg file-id) - bundle (p/-> (retrieve-bundle cfg file-id profile-id) + bundle (p/-> (retrieve-bundle cfg file-id profile-id components-v2) (assoc :permissions perms) (assoc-in [:file :thumbnails] thumbs))] diff --git a/backend/src/app/tasks/file_gc.clj b/backend/src/app/tasks/file_gc.clj index 29ad2eeb8..ec8b0a2e8 100644 --- a/backend/src/app/tasks/file_gc.clj +++ b/backend/src/app/tasks/file_gc.clj @@ -12,8 +12,8 @@ (:require [app.common.data :as d] [app.common.logging :as l] - [app.common.pages.helpers :as cph] [app.common.pages.migrations :as pmg] + [app.common.types.shape-tree :as ctt] [app.db :as db] [app.util.blob :as blob] [app.util.time :as dt] @@ -128,7 +128,7 @@ get-objects-ids (fn [{:keys [id objects]}] - (->> (cph/get-frames objects) + (->> (ctt/get-frames objects) (map #(str id (:id %))))) using (into #{} diff --git a/backend/test/app/services_files_test.clj b/backend/test/app/services_files_test.clj index aa188b215..c8ab0c25c 100644 --- a/backend/test/app/services_files_test.clj +++ b/backend/test/app/services_files_test.clj @@ -32,7 +32,8 @@ :project-id proj-id :id file-id :name "foobar" - :is-shared false} + :is-shared false + :components-v2 true} out (th/mutation! data)] ;; (th/print-result! out) @@ -71,7 +72,8 @@ (t/testing "query single file without users" (let [data {::th/type :file :profile-id (:id prof) - :id file-id} + :id file-id + :components-v2 true} out (th/query! data)] ;; (th/print-result! out) @@ -95,7 +97,8 @@ (t/testing "query single file after delete" (let [data {::th/type :file :profile-id (:id prof) - :id file-id} + :id file-id + :components-v2 true} out (th/query! data)] ;; (th/print-result! out) @@ -143,6 +146,7 @@ :session-id (uuid/random) :profile-id profile-id :revn revn + :components-v2 true :changes changes} out (th/mutation! params)] (t/is (nil? (:error out))) @@ -171,6 +175,7 @@ :id shid :parent-id uuid/zero :frame-id uuid/zero + :components-v2 true :obj {:id shid :name "image" :frame-id uuid/zero @@ -246,7 +251,8 @@ :profile-id (:id profile2) :project-id (:default-project-id profile1) :name "foobar" - :is-shared false} + :is-shared false + :components-v2 true} out (th/mutation! data) error (:error out)] @@ -462,6 +468,7 @@ (th/update-file* {:file-id (:id file) :profile-id (:id prof) :revn 0 + :components-v2 true :changes changes}) (t/testing "RPC page query (rendering purposes)" @@ -469,7 +476,8 @@ ;; Query :page RPC method without passing page-id (let [data {::th/type :page :profile-id (:id prof) - :file-id (:id file)} + :file-id (:id file) + :components-v2 true} {:keys [error result] :as out} (th/query! data)] ;; (th/print-result! out) @@ -485,7 +493,8 @@ (let [data {::th/type :page :profile-id (:id prof) :file-id (:id file) - :page-id page-id} + :page-id page-id + :components-v2 true} {:keys [error result] :as out} (th/query! data)] ;; (th/print-result! out) (t/is (map? result)) @@ -501,7 +510,8 @@ :profile-id (:id prof) :file-id (:id file) :page-id page-id - :object-id frame1-id} + :object-id frame1-id + :components-v2 true} {:keys [error result] :as out} (th/query! data)] ;; (th/print-result! out) (t/is (map? result)) @@ -516,7 +526,8 @@ (let [data {::th/type :page :profile-id (:id prof) :file-id (:id file) - :object-id frame1-id} + :object-id frame1-id + :components-v2 true} {:keys [error result] :as out} (th/query! data)] ;; (th/print-result! out) (t/is (= :validation (th/ex-type error))) @@ -537,7 +548,8 @@ ;; Check the result (let [data {::th/type :file-data-for-thumbnail :profile-id (:id prof) - :file-id (:id file)} + :file-id (:id file) + :components-v2 true} {:keys [error result] :as out} (th/query! data)] ;; (th/print-result! out) (t/is (map? result)) @@ -562,7 +574,8 @@ ;; Check the result (let [data {::th/type :file-data-for-thumbnail :profile-id (:id prof) - :file-id (:id file)} + :file-id (:id file) + :components-v2 true} {:keys [error result] :as out} (th/query! data)] ;; (th/print-result! out) (t/is (map? result)) diff --git a/backend/test/app/services_viewer_test.clj b/backend/test/app/services_viewer_test.clj index e8a01c255..86ad9189f 100644 --- a/backend/test/app/services_viewer_test.clj +++ b/backend/test/app/services_viewer_test.clj @@ -30,7 +30,8 @@ (let [data {::th/type :view-only-bundle :profile-id (:id prof) :file-id (:id file) - :page-id (get-in file [:data :pages 0])} + :page-id (get-in file [:data :pages 0]) + :components-v2 true} out (th/query! data)] @@ -63,7 +64,8 @@ (let [data {::th/type :view-only-bundle :profile-id (:id prof2) :file-id (:id file) - :page-id (get-in file [:data :pages 0])} + :page-id (get-in file [:data :pages 0]) + :components-v2 true} out (th/query! data)] ;; (th/print-result! out) @@ -78,7 +80,8 @@ :profile-id (:id prof2) :share-id @share-id :file-id (:id file) - :page-id (get-in file [:data :pages 0])} + :page-id (get-in file [:data :pages 0]) + :components-v2 true} out (th/query! data)] ;; (th/print-result! out) @@ -93,7 +96,8 @@ (let [data {::th/type :view-only-bundle :share-id @share-id :file-id (:id file) - :page-id (get-in file [:data :pages 0])} + :page-id (get-in file [:data :pages 0]) + :components-v2 true} out (th/query! data)] ;; (th/print-result! out) diff --git a/backend/test/app/test_helpers.clj b/backend/test/app/test_helpers.clj index 8849ac3cc..8f436ea02 100644 --- a/backend/test/app/test_helpers.clj +++ b/backend/test/app/test_helpers.clj @@ -164,7 +164,8 @@ (us/assert uuid? project-id) (#'files/create-file conn (merge {:id (mk-uuid "file" i) - :name (str "file" i)} + :name (str "file" i) + :components-v2 true} params)))) (defn mark-file-deleted* @@ -249,6 +250,7 @@ :metrics metrics} {:file file :revn revn + :components-v2 true :changes changes :session-id session-id :profile-id profile-id})))) diff --git a/common/src/app/common/data.cljc b/common/src/app/common/data.cljc index baa0f2fe3..d03a935a2 100644 --- a/common/src/app/common/data.cljc +++ b/common/src/app/common/data.cljc @@ -173,6 +173,10 @@ [data] (into {} (remove (comp nil? second)) data)) +(defn vec-without-nils + [coll] + (into [] (remove nil?) coll)) + (defn without-qualified [data] (into {} (remove (comp qualified-keyword? first)) data)) diff --git a/common/src/app/common/file_builder.cljc b/common/src/app/common/file_builder.cljc index 882991a1f..a924186b3 100644 --- a/common/src/app/common/file_builder.cljc +++ b/common/src/app/common/file_builder.cljc @@ -12,8 +12,10 @@ [app.common.geom.shapes :as gsh] [app.common.pages.changes :as ch] [app.common.pages.changes-spec :as pcs] - [app.common.pages.init :as init] [app.common.spec :as us] + [app.common.types.file :as ctf] + [app.common.types.page :as ctp] + [app.common.types.shape :as cts] [app.common.uuid :as uuid] [cuerdas.core :as str])) @@ -167,7 +169,7 @@ ([id name] {:id id :name name - :data (-> init/empty-file-data + :data (-> ctf/empty-file-data (assoc :id id)) ;; We keep the changes so we can send them to the backend @@ -178,8 +180,7 @@ (assert (nil? (:current-component-id file))) (let [page-id (or (:id data) (uuid/next)) - page (-> init/empty-page-data - (assoc :id page-id) + page (-> (ctp/make-empty-page page-id "Page-1") (d/deep-merge data))] (-> file (commit-change @@ -208,7 +209,7 @@ (defn add-artboard [file data] (assert (nil? (:current-component-id file))) - (let [obj (-> (init/make-minimal-shape :frame) + (let [obj (-> (cts/make-minimal-shape :frame) (merge data) (check-name file :frame) (setup-selrect) @@ -232,9 +233,9 @@ (defn add-group [file data] (let [frame-id (:current-frame-id file) - selrect init/empty-selrect + selrect cts/empty-selrect name (:name data) - obj (-> (init/make-minimal-group frame-id selrect name) + obj (-> (cts/make-minimal-group frame-id selrect name) (merge data) (check-name file :group) (d/without-nils))] @@ -346,7 +347,7 @@ (update :parent-stack pop)))) (defn create-shape [file type data] - (let [obj (-> (init/make-minimal-shape type) + (let [obj (-> (cts/make-minimal-shape type) (merge data) (check-name file :type) (setup-selrect) @@ -514,10 +515,10 @@ (defn start-component [file data] - (let [selrect init/empty-selrect + (let [selrect cts/empty-selrect name (:name data) path (:path data) - obj (-> (init/make-minimal-group nil selrect name) + obj (-> (cts/make-minimal-group nil selrect name) (merge data) (check-name file :group) (d/without-nils))] diff --git a/common/src/app/common/geom/shapes.cljc b/common/src/app/common/geom/shapes.cljc index 31c103779..a08fe8a21 100644 --- a/common/src/app/common/geom/shapes.cljc +++ b/common/src/app/common/geom/shapes.cljc @@ -41,13 +41,23 @@ ;; --- Helpers -(defn left-bound +(defn bounding-box + "Returns a rect that wraps the shape after all transformations applied." [shape] - (get shape :x (:x (:selrect shape)))) ; Paths don't have :x attribute + ; TODO: perhaps we need to store this calculation in a shape attribute + (gpr/points->rect (:points shape))) + +(defn left-bound + "Returns the lowest x coord of the shape BEFORE applying transformations." + ; TODO: perhaps some day we want after transformations, but for the + ; moment it's enough as is now. + [shape] + (or (:x shape) (:x (:selrect shape)))) ; Paths don't have :x attribute (defn top-bound + "Returns the lowest y coord of the shape BEFORE applying transformations." [shape] - (get shape :y (:y (:selrect shape)))) ; Paths don't have :y attribute + (or (:y shape) (:y (:selrect shape)))) ; Paths don't have :y attribute (defn fully-contained? "Checks if one rect is fully inside the other" diff --git a/common/src/app/common/pages.cljc b/common/src/app/common/pages.cljc index e500d372d..b4c43458c 100644 --- a/common/src/app/common/pages.cljc +++ b/common/src/app/common/pages.cljc @@ -12,7 +12,7 @@ [app.common.pages.common :as common] [app.common.pages.focus :as focus] [app.common.pages.indices :as indices] - [app.common.pages.init :as init])) + [app.common.types.file :as ctf])) ;; Common (dm/export common/root) @@ -36,11 +36,5 @@ (dm/export changes/process-changes) ;; Initialization -(dm/export init/default-frame-attrs) -(dm/export init/default-shape-attrs) -(dm/export init/make-file-data) -(dm/export init/make-minimal-shape) -(dm/export init/make-minimal-group) -(dm/export init/empty-file-data) -(dm/export init/setup-shape) -(dm/export init/setup-rect-selrect) +(dm/export ctf/make-file-data) +(dm/export ctf/empty-file-data) diff --git a/common/src/app/common/pages/changes.cljc b/common/src/app/common/pages/changes.cljc index 36a8a576c..152612769 100644 --- a/common/src/app/common/pages/changes.cljc +++ b/common/src/app/common/pages/changes.cljc @@ -14,10 +14,16 @@ [app.common.math :as mth] [app.common.pages.common :refer [component-sync-attrs]] [app.common.pages.helpers :as cph] - [app.common.pages.init :as init] [app.common.spec :as us] [app.common.pages.changes-spec :as pcs] - [app.common.types.shape :as cts])) + [app.common.types.components-list :as ctkl] + [app.common.types.container :as ctn] + [app.common.types.colors-list :as ctcl] + [app.common.types.page :as ctp] + [app.common.types.pages-list :as ctpl] + [app.common.types.shape :as cts] + [app.common.types.shape-tree :as ctst] + [app.common.types.typographies-list :as ctyl])) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Specific helpers @@ -28,15 +34,11 @@ [coll o] (into [] (filter #(not= % o)) coll)) -(defn vec-without-nils - [coll] - (into [] (remove nil?) coll)) - ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Page Transformation Changes ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -;; --- Changes Processing Impl +;; === Changes Processing Impl (defmulti process-change (fn [_ change] (:type change))) (defmulti process-operation (fn [_ op] (:type op))) @@ -74,44 +76,9 @@ (defmethod process-change :add-obj [data {:keys [id obj page-id component-id frame-id parent-id index ignore-touched]}] - (letfn [(update-parent-shapes [shapes] - ;; Ensure that shapes is always a vector. - (let [shapes (into [] shapes)] - (cond - (some #{id} shapes) - shapes - - (nil? index) - (conj shapes id) - - :else - (cph/insert-at-index shapes index [id])))) - - (update-parent [parent] - (-> parent - (update :shapes update-parent-shapes) - (update :shapes vec-without-nils) - (cond-> (and (:shape-ref parent) - (not= (:id parent) frame-id) - (not ignore-touched)) - (-> (update :touched cph/set-touched-group :shapes-group) - (dissoc :remote-synced?))))) - - ;; TODO: this looks wrong, why we allow nil values? - (update-objects [objects parent-id] - (if (and (or (nil? parent-id) (contains? objects parent-id)) - (or (nil? frame-id) (contains? objects frame-id))) - (-> objects - (assoc id (-> obj - (assoc :frame-id frame-id) - (assoc :parent-id parent-id) - (assoc :id id))) - (update parent-id update-parent)) - objects)) - - (update-container [data] - (let [parent-id (or parent-id frame-id)] - (update data :objects update-objects parent-id)))] + (let [update-container + (fn [container] + (ctst/add-shape id obj container frame-id parent-id index ignore-touched))] (if page-id (d/update-in-when data [:pages-index page-id] update-container) @@ -237,7 +204,7 @@ ;; We need to ensure that no `nil` in the ;; shapes list after adding all the ;; incoming shapes to the parent. - (update :shapes vec-without-nils))] + (update :shapes d/vec-without-nils))] (cond-> parent (and (:shape-ref parent) (= (:type parent) :group) (not ignore-touched)) (-> (update :touched cph/set-touched-group :shapes-group) @@ -258,7 +225,7 @@ (-> objects (d/update-in-when [pid :shapes] without-obj sid) - (d/update-in-when [pid :shapes] vec-without-nils) + (d/update-in-when [pid :shapes] d/vec-without-nils) (cond-> component? (d/update-when pid #(-> % (update :touched cph/set-touched-group :shapes-group) (dissoc :remote-synced?))))))))) @@ -323,22 +290,11 @@ [data {:keys [id name page]}] (when (and id name page) (ex/raise :type :conflict - :hint "name or page should be provided, never both")) - (letfn [(conj-if-not-exists [pages id] - (cond-> pages - (not (d/seek #(= % id) pages)) - (conj id)))] - (if (and (string? name) (uuid? id)) - (let [page (assoc init/empty-page-data - :id id - :name name)] - (-> data - (update :pages conj-if-not-exists id) - (update :pages-index assoc id page))) - - (-> data - (update :pages conj-if-not-exists (:id page)) - (update :pages-index assoc (:id page) page))))) + :hint "id+name or page should be provided, never both")) + (let [page (if (and (string? name) (uuid? id)) + (ctp/make-empty-page id name) + page)] + (ctpl/add-page data page))) (defmethod process-change :mod-page [data {:keys [id name]}] @@ -356,7 +312,7 @@ (defmethod process-change :add-color [data {:keys [color]}] - (update data :colors assoc (:id color) color)) + (ctcl/add-color data color)) (defmethod process-change :mod-color [data {:keys [color]}] @@ -392,12 +348,14 @@ ;; -- Components (defmethod process-change :add-component - [data {:keys [id name path shapes]}] - (assoc-in data [:components id] - {:id id - :name name - :path path - :objects (d/index-by :id shapes)})) + [data {:keys [id name path main-instance-id main-instance-page shapes]}] + (ctkl/add-component data + id + name + path + main-instance-id + main-instance-page + shapes)) (defmethod process-change :mod-component [data {:keys [id name path objects]}] @@ -420,7 +378,7 @@ (defmethod process-change :add-typography [data {:keys [typography]}] - (update data :typographies assoc (:id typography) typography)) + (ctyl/add-typography data typography)) (defmethod process-change :mod-typography [data {:keys [typography]}] @@ -430,7 +388,7 @@ [data {:keys [id]}] (update data :typographies dissoc id)) -;; -- Operations +;; === Operations (defmethod process-operation :set [shape op] @@ -494,3 +452,36 @@ (ex/raise :type :not-implemented :code :operation-not-implemented :context {:type (:type op)})) + + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; Component changes detection +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +;; Analyze one change and checks if if modifies the main instance of +;; any component, so that it needs to be synced immediately to the +;; main component. Return the ids of the components that need sync. + +(defmulti components-changed (fn [_ change] (:type change))) + +(defmethod components-changed :mod-obj + [file-data {:keys [id page-id _component-id operations]}] + (when page-id + (let [page (ctpl/get-page file-data page-id) + shape-and-parents (map #(ctn/get-shape page %) + (into [id] (cph/get-parent-ids (:objects page) id))) + need-sync? (fn [operation] + ; We need to trigger a sync if the shape has changed any + ; attribute that participates in components syncronization. + (and (= (:type operation) :set) + (component-sync-attrs (:attr operation)))) + any-sync? (some need-sync? operations)] + (when any-sync? + (let [xform (comp (filter :main-instance?) ; Select shapes that are main component instances + (map :id))] + (into #{} xform shape-and-parents)))))) + +(defmethod components-changed :default + [_ _] + nil) + diff --git a/common/src/app/common/pages/changes_builder.cljc b/common/src/app/common/pages/changes_builder.cljc index 25808b136..671e6a636 100644 --- a/common/src/app/common/pages/changes_builder.cljc +++ b/common/src/app/common/pages/changes_builder.cljc @@ -16,6 +16,7 @@ [app.common.math :as mth] [app.common.pages :as cp] [app.common.pages.helpers :as cph] + [app.common.types.file :as ctf] [app.common.uuid :as uuid])) ;; Auxiliary functions to help create a set of changes (undo + redo) @@ -49,7 +50,7 @@ (defn with-objects [changes objects] - (let [file-data (-> (cp/make-file-data (uuid/next) uuid/zero) + (let [file-data (-> (ctf/make-file-data (uuid/next) uuid/zero true) (assoc-in [:pages-index uuid/zero :objects] objects))] (vary-meta changes assoc ::file-data file-data ::applied-changes-count 0))) @@ -111,7 +112,9 @@ redo-changes (:redo-changes changes) new-changes (if (< index (count redo-changes)) (->> (subvec (:redo-changes changes) index) - (map #(assoc % :page-id uuid/zero))) + (map #(-> % + (assoc :page-id uuid/zero) + (dissoc :component-id)))) []) new-file-data (cp/process-changes file-data new-changes)] (vary-meta changes assoc ::file-data new-file-data @@ -223,6 +226,15 @@ (update :undo-changes d/preconj del-change) (apply-changes-local))))) +(defn add-objects + ([changes objects] + (add-objects changes objects nil)) + + ([changes objects params] + (reduce #(add-object %1 %2 params) + changes + objects))) + (defn change-parent ([changes parent-id shapes] (change-parent changes parent-id shapes nil)) @@ -532,7 +544,7 @@ (apply-changes-local)))) (defn add-component - [changes id path name new-shapes updated-shapes] + [changes id path name new-shapes updated-shapes main-instance-id main-instance-page] (assert-page-id changes) (assert-objects changes) (let [page-id (::page-id (meta changes)) @@ -552,6 +564,9 @@ {:type :set :attr :component-root? :val (:component-root? shape)} + {:type :set + :attr :main-instance? + :val (:main-instance? shape)} {:type :set :attr :shape-ref :val (:shape-ref shape)} @@ -566,6 +581,8 @@ :id id :path path :name name + :main-instance-id main-instance-id + :main-instance-page main-instance-page :shapes new-shapes}) (into (map mk-change) updated-shapes)))) (update :undo-changes @@ -611,5 +628,7 @@ :id id :name (:name prev-component) :path (:path prev-component) + :main-instance-id (:main-instance-id prev-component) + :main-instance-page (:main-instance-page prev-component) :shapes (vals (:objects prev-component))})))) diff --git a/common/src/app/common/pages/focus.cljc b/common/src/app/common/pages/focus.cljc index a7ca0f495..57a048b2e 100644 --- a/common/src/app/common/pages/focus.cljc +++ b/common/src/app/common/pages/focus.cljc @@ -8,6 +8,7 @@ (:require [app.common.data :as d] [app.common.pages.helpers :as cph] + [app.common.types.shape-tree :as ctt] [app.common.uuid :as uuid])) (defn focus-objects @@ -21,7 +22,7 @@ (cond-> objects (some? ids-with-children) (-> (select-keys ids-with-children) - (assoc-in [uuid/zero :shapes] (cph/sort-z-index objects focus)))))) + (assoc-in [uuid/zero :shapes] (ctt/sort-z-index objects focus)))))) (defn filter-not-focus [objects focus ids] diff --git a/common/src/app/common/pages/helpers.cljc b/common/src/app/common/pages/helpers.cljc index 044d97346..23424981c 100644 --- a/common/src/app/common/pages/helpers.cljc +++ b/common/src/app/common/pages/helpers.cljc @@ -8,10 +8,7 @@ (:require [app.common.data :as d] [app.common.data.macros :as dm] - [app.common.geom.shapes :as gsh] - [app.common.math :as mth] [app.common.spec :as us] - [app.common.types.page :as ctp] [app.common.uuid :as uuid] [cuerdas.core :as str])) @@ -62,14 +59,6 @@ (and (not (frame-shape? shape)) (= (:frame-id shape) uuid/zero))) -(defn get-shape - [container shape-id] - (us/assert ::ctp/container container) - (us/assert ::us/uuid shape-id) - (-> container - (get :objects) - (get shape-id))) - (defn get-children-ids [objects id] (if-let [shapes (-> (get objects id) :shapes (some-> vec))] @@ -158,146 +147,6 @@ (:shapes) (keep lookup))))) -(defn get-frames - "Retrieves all frame objects as vector" - [objects] - (or (-> objects meta ::index-frames) - (let [lookup (d/getf objects) - xform (comp (remove #(= uuid/zero %)) - (keep lookup) - (filter frame-shape?))] - (->> (keys objects) - (into [] xform))))) - -(defn get-frames-ids - "Retrieves all frame ids as vector" - [objects] - (->> (get-frames objects) - (mapv :id))) - -(defn get-nested-frames - [objects frame-id] - (into #{} - (comp (filter frame-shape?) - (map :id)) - (get-children objects frame-id))) - -(defn get-root-frames-ids - "Retrieves all frame objects as vector. It is not implemented in - function of `get-immediate-children` for performance reasons. This - function is executed in the render hot path." - [objects] - (let [add-frame - (fn [result shape] - (cond-> result - (frame-shape? shape) - (conj (:id shape))))] - (reduce-objects objects (complement frame-shape?) add-frame []))) - -(defn get-root-objects - "Get all the objects under the root object" - [objects] - (let [add-shape - (fn [result shape] - (conj result shape))] - (reduce-objects objects (complement frame-shape?) add-shape []))) - -(defn get-root-shapes - "Get all shapes that are not frames" - [objects] - (let [add-shape - (fn [result shape] - (cond-> result - (not (frame-shape? shape)) - (conj shape)))] - (reduce-objects objects (complement frame-shape?) add-shape []))) - -(defn get-root-shapes-ids - [objects] - (->> (get-root-shapes objects) - (mapv :id))) - -(defn get-base - [objects id-a id-b] - - (let [parents-a (reverse (get-parents-seq objects id-a)) - parents-b (reverse (get-parents-seq objects id-b)) - - [base base-child-a base-child-b] - (loop [parents-a (rest parents-a) - parents-b (rest parents-b) - base uuid/zero] - (cond - (not= (first parents-a) (first parents-b)) - [base (first parents-a) (first parents-b)] - - (or (empty? parents-a) (empty? parents-b)) - [uuid/zero (first parents-a) (first parents-b)] - - :else - (recur (rest parents-a) (rest parents-b) (first parents-a)))) - - index-base-a (when base-child-a (get-position-on-parent objects base-child-a)) - index-base-b (when base-child-b (get-position-on-parent objects base-child-b))] - - [base index-base-a index-base-b])) - -(defn is-shape-over-shape? - [objects base-shape-id over-shape-id {:keys [top-frames?]}] - - (let [[base index-a index-b] (get-base objects base-shape-id over-shape-id)] - (cond - (= base base-shape-id) - (and (not top-frames?) - (frame-shape? objects base-shape-id) - (root-frame? objects base-shape-id)) - - (= base over-shape-id) - (or top-frames? - (not (frame-shape? objects over-shape-id)) - (not (root-frame? objects over-shape-id))) - - :else - (< index-a index-b)))) - -(defn sort-z-index - ([objects ids] - (sort-z-index objects ids nil)) - - ([objects ids {:keys [bottom-frames?] :as options}] - (letfn [(comp [id-a id-b] - (let [type-a (dm/get-in objects [id-a :type]) - type-b (dm/get-in objects [id-b :type])] - (cond - (and bottom-frames? (= :frame type-a) (not= :frame type-b)) - 1 - - (and bottom-frames? (not= :frame type-a) (= :frame type-b)) - -1 - - (= id-a id-b) - 0 - - (is-shape-over-shape? objects id-a id-b options) - 1 - - :else - -1)))] - (sort comp ids)))) - -(defn frame-id-by-position - [objects position] - (let [top-frame - (->> (get-frames-ids objects) - (sort-z-index objects) - (d/seek #(and position (gsh/has-point? (get objects %) position))))] - (or top-frame uuid/zero))) - -(defn frame-by-position - [objects position] - (let [frame-id (frame-id-by-position objects position)] - (get objects frame-id))) - (declare indexed-shapes) (defn get-base-shape @@ -356,16 +205,6 @@ ([libraries library-id component-id] (get-in libraries [library-id :data :components component-id]))) -(defn is-main-of? - [shape-main shape-inst] - (and (:shape-ref shape-inst) - (or (= (:shape-ref shape-inst) (:id shape-main)) - (= (:shape-ref shape-inst) (:shape-ref shape-main))))) - -(defn get-component-root - [component] - (get-in component [:objects (:id component)])) - (defn get-component-shape "Get the parent shape linked to a component for this shape, if any" [objects shape] @@ -462,57 +301,6 @@ (reduce add-element (d/ordered-set) ids))) -(defn clone-object - "Gets a copy of the object and all its children, with new ids - and with the parent-children links correctly set. Admits functions - to make more transformations to the cloned objects and the - original ones. - - Returns the cloned object, the list of all new objects (including - the cloned one), and possibly a list of original objects modified." - - ([object parent-id objects update-new-object] - (clone-object object parent-id objects update-new-object identity)) - - ([object parent-id objects update-new-object update-original-object] - (let [new-id (uuid/next)] - (loop [child-ids (seq (:shapes object)) - new-direct-children [] - new-children [] - updated-children []] - - (if (empty? child-ids) - (let [new-object (cond-> object - true - (assoc :id new-id - :parent-id parent-id) - - (some? (:shapes object)) - (assoc :shapes (mapv :id new-direct-children))) - - new-object (update-new-object new-object object) - new-objects (into [new-object] new-children) - - updated-object (update-original-object object new-object) - updated-objects (if (identical? object updated-object) - updated-children - (into [updated-object] updated-children))] - - [new-object new-objects updated-objects]) - - (let [child-id (first child-ids) - child (get objects child-id) - _ (us/assert some? child) - - [new-child new-child-objects updated-child-objects] - (clone-object child new-id objects update-new-object update-original-object)] - - (recur - (next child-ids) - (into new-direct-children [new-child]) - (into new-children new-child-objects) - (into updated-children updated-child-objects)))))))) - (defn indexed-shapes "Retrieves a list with the indexes for each element in the layer tree. This will be used for shift+selection." @@ -695,36 +483,3 @@ :id)) -(defn get-viewer-frames - ([objects] - (get-viewer-frames objects nil)) - - ([objects {:keys [all-frames?]}] - (into [] - (comp (map (d/getf objects)) - (if all-frames? - (map identity) - (remove :hide-in-viewer))) - (sort-z-index objects (get-frames-ids objects) {:top-frames? true})))) - -(defn start-page-index - [objects] - (with-meta objects {::index-frames (get-frames (with-meta objects nil))})) - -(defn update-page-index - [objects] - (with-meta objects {::index-frames (get-frames (with-meta objects nil))})) - -(defn start-object-indices - [file] - (letfn [(process-index [page-index page-id] - (update-in page-index [page-id :objects] start-page-index))] - (update file :pages-index #(reduce process-index % (keys %))))) - -(defn update-object-indices - [file page-id] - (update-in file [:pages-index page-id :objects] update-page-index)) - -(defn rotated-frame? - [frame] - (not (mth/almost-zero? (:rotation frame 0)))) diff --git a/common/src/app/common/pages/init.cljc b/common/src/app/common/pages/init.cljc deleted file mode 100644 index 3b1e85b04..000000000 --- a/common/src/app/common/pages/init.cljc +++ /dev/null @@ -1,186 +0,0 @@ -;; 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.pages.init - (:require - [app.common.colors :as clr] - [app.common.data :as d] - [app.common.exceptions :as ex] - [app.common.geom.shapes :as gsh] - [app.common.pages.common :refer [file-version default-color]] - [app.common.uuid :as uuid])) - -(def root uuid/zero) - -(def empty-page-data - {:options {} - :name "Page-1" - :objects - {root - {:id root - :type :frame - :name "Root Frame"}}}) - -(def empty-file-data - {:version file-version - :pages [] - :pages-index {}}) - -(def default-shape-attrs - {}) - -(def default-frame-attrs - {:frame-id uuid/zero - :fills [{:fill-color clr/white - :fill-opacity 1}] - :strokes [] - :shapes [] - :hide-fill-on-export false}) - -(def ^:private minimal-shapes - [{:type :rect - :name "Rect-1" - :fills [{:fill-color default-color - :fill-opacity 1}] - :strokes [] - :rx 0 - :ry 0} - - {:type :image - :rx 0 - :ry 0 - :fills [] - :strokes []} - - {:type :circle - :name "Circle-1" - :fills [{:fill-color default-color - :fill-opacity 1}] - :strokes []} - - {:type :path - :name "Path-1" - :fills [] - :strokes [{:stroke-style :solid - :stroke-alignment :center - :stroke-width 2 - :stroke-color clr/black - :stroke-opacity 1}]} - - {:type :frame - :name "Board-1" - :fills [{:fill-color clr/white - :fill-opacity 1}] - :strokes [] - :stroke-style :none - :stroke-alignment :center - :stroke-width 0 - :stroke-color clr/black - :stroke-opacity 0 - :rx 0 - :ry 0} - - {:type :text - :name "Text-1" - :content nil} - - {:type :svg-raw}]) - -(def empty-selrect - {:x 0 :y 0 - :x1 0 :y1 0 - :x2 0.01 :y2 0.01 - :width 0.01 :height 0.01}) - -(defn make-minimal-shape - [type] - (let [type (cond (= type :curve) :path - :else type) - shape (d/seek #(= type (:type %)) minimal-shapes)] - (when-not shape - (ex/raise :type :assertion - :code :shape-type-not-implemented - :context {:type type})) - - (cond-> shape - :always - (assoc :id (uuid/next)) - - (not= :path (:type shape)) - (assoc :x 0 - :y 0 - :width 0.01 - :height 0.01 - :selrect {:x 0 - :y 0 - :x1 0 - :y1 0 - :x2 0.01 - :y2 0.01 - :width 0.01 - :height 0.01})))) - -(defn make-minimal-group - [frame-id selection-rect group-name] - {:id (uuid/next) - :type :group - :name group-name - :shapes [] - :frame-id frame-id - :x (:x selection-rect) - :y (:y selection-rect) - :width (:width selection-rect) - :height (:height selection-rect)}) - -(defn make-file-data - ([file-id] - (make-file-data file-id (uuid/next))) - - ([file-id page-id] - (let [pd (assoc empty-page-data - :id page-id - :name "Page-1")] - (-> empty-file-data - (assoc :id file-id) - (update :pages conj page-id) - (update :pages-index assoc page-id pd))))) - -(defn setup-rect-selrect - "Initializes the selrect and points for a shape" - [shape] - (let [selrect (gsh/rect->selrect shape) - points (gsh/rect->points shape)] - (-> shape - (assoc :selrect selrect - :points points)))) - -(defn- setup-rect - "A specialized function for setup rect-like shapes." - [shape {:keys [x y width height]}] - (-> shape - (assoc :x x :y y :width width :height height) - (setup-rect-selrect))) - -(defn- setup-image - [{:keys [metadata] :as shape} props] - (-> (setup-rect shape props) - (assoc - :proportion (/ (:width metadata) - (:height metadata)) - :proportion-lock true))) - -(defn setup-shape - "A function that initializes the first coordinates for - the shape. Used mainly for draw operations." - ([props] - (setup-shape {:type :rect} props)) - - ([shape props] - (case (:type shape) - :image (setup-image shape props) - (setup-rect shape props)))) - - diff --git a/common/src/app/common/pages/migrations.cljc b/common/src/app/common/pages/migrations.cljc index d2beb3ce0..7807ea3ee 100644 --- a/common/src/app/common/pages/migrations.cljc +++ b/common/src/app/common/pages/migrations.cljc @@ -15,6 +15,7 @@ [app.common.math :as mth] [app.common.pages :as cp] [app.common.pages.helpers :as cph] + [app.common.types.shape :as cts] [app.common.uuid :as uuid] [cuerdas.core :as str])) @@ -84,7 +85,7 @@ (fix-empty-points [shape] (let [shape (cond-> shape - (empty? (:selrect shape)) (cp/setup-rect-selrect))] + (empty? (:selrect shape)) (cts/setup-rect-selrect))] (cond-> shape (empty? (:points shape)) (assoc :points (gsh/rect->points (:selrect shape)))))) diff --git a/common/src/app/common/spec.cljc b/common/src/app/common/spec.cljc index e596179ad..a1fb2fd72 100644 --- a/common/src/app/common/spec.cljc +++ b/common/src/app/common/spec.cljc @@ -107,6 +107,7 @@ (s/def ::number (s/conformer number-conformer str)) (s/def ::integer (s/conformer integer-conformer str)) (s/def ::not-empty-string (s/and string? #(not (str/empty? %)))) +(s/def ::set-of-string (s/every string? :kind set?)) (s/def ::url string?) (s/def ::fn fn?) (s/def ::id ::uuid) diff --git a/common/src/app/common/types/color.cljc b/common/src/app/common/types/color.cljc index 674de1cc2..06c5c497b 100644 --- a/common/src/app/common/types/color.cljc +++ b/common/src/app/common/types/color.cljc @@ -102,6 +102,12 @@ :fill-opacity opacity :fill-color-gradient gradient))))) +(defn attach-fill-color + [shape position ref-id ref-file] + (-> shape + (assoc-in [:fills position :fill-color-ref-id] ref-id) + (assoc-in [:fills position :fill-color-ref-file] ref-file))) + (defn detach-fill-color [shape position] (-> shape @@ -127,6 +133,12 @@ :stroke-opacity opacity :stroke-color-gradient gradient))))) +(defn attach-stroke-color + [shape position ref-id ref-file] + (-> shape + (assoc-in [:strokes position :stroke-color-ref-id] ref-id) + (assoc-in [:strokes position :stroke-color-ref-file] ref-file))) + (defn detach-stroke-color [shape position] (-> shape @@ -152,6 +164,12 @@ :opacity opacity :gradient gradient))))) +(defn attach-shadow-color + [shape position ref-id ref-file] + (-> shape + (assoc-in [:shadow position :color :id] ref-id) + (assoc-in [:shadow position :color :file-id] ref-file))) + (defn detach-shadow-color [shape position] (-> shape @@ -176,6 +194,11 @@ :color color :opacity opacity :gradient gradient))))) +(defn attach-grid-color + [shape position ref-id ref-file] + (-> shape + (assoc-in [:grids position :params :color :id] ref-id) + (assoc-in [:grids position :params :color :file-id] ref-file))) (defn detach-grid-color [shape position] @@ -213,65 +236,97 @@ (= (:ref-file %) library-id)) all-colors))) +(defn uses-library-color? + "Check if the shape uses the given library color." + [shape library-id color-id] + (let [all-colors (get-all-colors shape)] + (some #(and (= (:ref-id %) color-id) + (= (:ref-file %) library-id)) + all-colors))) + +(defn- process-shape-colors + "Execute an update function on all colors of a shape." + [shape process-fn] + (let [process-fill (fn [shape [position fill]] + (process-fn shape + position + (fill->shape-color fill) + set-fill-color + attach-fill-color + detach-fill-color)) + + process-stroke (fn [shape [position stroke]] + (process-fn shape + position + (stroke->shape-color stroke) + set-stroke-color + attach-stroke-color + detach-stroke-color)) + + process-shadow (fn [shape [position shadow]] + (process-fn shape + position + (shadow->shape-color shadow) + set-shadow-color + attach-shadow-color + detach-shadow-color)) + + process-grid (fn [shape [position grid]] + (process-fn shape + position + (grid->shape-color grid) + set-grid-color + attach-grid-color + detach-grid-color)) + + process-text-node (fn [node] + (as-> node $ + (reduce process-fill $ (d/enumerate (:fills $))) + (reduce process-stroke $ (d/enumerate (:strokes $))))) + + process-text (fn [shape] + (let [content (:content shape) + new-content (txt/transform-nodes process-text-node content)] + (if (not= content new-content) + (assoc shape :content new-content) + shape)))] + + (as-> shape $ + (reduce process-fill $ (d/enumerate (:fills $))) + (reduce process-stroke $ (d/enumerate (:strokes $))) + (reduce process-shadow $ (d/enumerate (:shadow $))) + (reduce process-grid $ (d/enumerate (:grids $))) + (process-text $)))) + +(defn remap-colors + "Change the shape so that any use of the given color now points to + the given library." + [shape library-id color] + (letfn [(remap-color [shape position shape-color _ attach-fn _] + (if (= (:ref-id shape-color) (:id color)) + (attach-fn shape + position + (:id color) + library-id) + shape))] + + (process-shape-colors shape remap-color))) + (defn sync-shape-colors "Look for usage of any color of the given library inside the shape, and, in this case, copy the library color into the shape." [shape library-id library-colors] - (let [sync-color (fn [shape position shape-color set-fn detach-fn] - (if (= (:ref-file shape-color) library-id) - (let [library-color (get library-colors (:ref-id shape-color))] - (if (some? library-color) - (set-fn shape - position - (:color library-color) - (:opacity library-color) - (:gradient library-color)) - (detach-fn shape position))) - shape)) + (letfn [(sync-color [shape position shape-color set-fn _ detach-fn] + (if (= (:ref-file shape-color) library-id) + (let [library-color (get library-colors (:ref-id shape-color))] + (if (some? library-color) + (set-fn shape + position + (:color library-color) + (:opacity library-color) + (:gradient library-color)) + (detach-fn shape position))) + shape))] - sync-fill (fn [shape [position fill]] - (sync-color shape - position - (fill->shape-color fill) - set-fill-color - detach-fill-color)) + (process-shape-colors shape sync-color))) - sync-stroke (fn [shape [position stroke]] - (sync-color shape - position - (stroke->shape-color stroke) - set-stroke-color - detach-stroke-color)) - - sync-shadow (fn [shape [position shadow]] - (sync-color shape - position - (shadow->shape-color shadow) - set-shadow-color - detach-shadow-color)) - - sync-grid (fn [shape [position grid]] - (sync-color shape - position - (grid->shape-color grid) - set-grid-color - detach-grid-color)) - - sync-text-node (fn [node] - (as-> node $ - (reduce sync-fill $ (d/enumerate (:fills $))) - (reduce sync-stroke $ (d/enumerate (:strokes $))))) - - sync-text (fn [shape] - (let [content (:content shape) - new-content (txt/transform-nodes sync-text-node content)] - (if (not= content new-content) - (assoc shape :content new-content) - shape)))] - - (as-> shape $ - (reduce sync-fill $ (d/enumerate (:fills $))) - (reduce sync-stroke $ (d/enumerate (:strokes $))) - (reduce sync-shadow $ (d/enumerate (:shadow $))) - (reduce sync-grid $ (d/enumerate (:grids $))) - (sync-text $)))) diff --git a/common/src/app/common/types/colors_list.cljc b/common/src/app/common/types/colors_list.cljc new file mode 100644 index 000000000..7c1b7ba0a --- /dev/null +++ b/common/src/app/common/types/colors_list.cljc @@ -0,0 +1,24 @@ +;; 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.colors-list) + +(defn colors-seq + [file-data] + (vals (:colors file-data))) + +(defn add-color + [file-data color] + (update file-data :colors assoc (:id color) color)) + +(defn get-color + [file-data color-id] + (get-in file-data [:colors color-id])) + +(defn update-color + [file-data color-id f] + (update-in file-data [:colors color-id] f)) + diff --git a/common/src/app/common/types/component.cljc b/common/src/app/common/types/component.cljc new file mode 100644 index 000000000..279b8e247 --- /dev/null +++ b/common/src/app/common/types/component.cljc @@ -0,0 +1,36 @@ +;; 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 file-id component-id] + (and (some? (:component-id shape)) + (some? (:component-file shape)) + (= (:component-id shape) component-id) + (= (:component-file shape) file-id))) + +(defn is-main-of? + [shape-main shape-inst] + (and (:shape-ref shape-inst) + (or (= (:shape-ref shape-inst) (:id shape-main)) + (= (:shape-ref shape-inst) (:shape-ref shape-main))))) + +(defn is-main-instance? + [shape-id page-id component] + (and (= shape-id (:main-instance-id component)) + (= page-id (:main-instance-page component)))) + +(defn get-component-root + [component] + (get-in component [:objects (:id component)])) + +(defn uses-library-components? + "Check if the shape uses any component in the given library." + [shape library-id] + (and (some? (:component-id shape)) + (= (:component-file shape) library-id))) + diff --git a/common/src/app/common/types/components_list.cljc b/common/src/app/common/types/components_list.cljc new file mode 100644 index 000000000..d2bbff58f --- /dev/null +++ b/common/src/app/common/types/components_list.cljc @@ -0,0 +1,37 @@ +;; 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.components-list + (:require + [app.common.data :as d])) + +(defn components-seq + [file-data] + (vals (:components file-data))) + +(defn add-component + [file-data id name path main-instance-id main-instance-page shapes] + (let [components-v2 (get-in file-data [:options :components-v2])] + (cond-> file-data + :always + (assoc-in [:components id] + {:id id + :name name + :path path + :objects (d/index-by :id shapes)}) + + components-v2 + (update-in [:components id] assoc :main-instance-id main-instance-id + :main-instance-page main-instance-page)))) + +(defn get-component + [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 new file mode 100644 index 000000000..b6ea7b917 --- /dev/null +++ b/common/src/app/common/types/container.cljc @@ -0,0 +1,160 @@ +;; 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.container + (:require + [app.common.geom.point :as gpt] + [app.common.geom.shapes :as gsh] + [app.common.spec :as us] + [app.common.types.shape-tree :as ctst] + [clojure.spec.alpha :as s])) + +(s/def ::type #{:page :component}) +(s/def ::id uuid?) +(s/def ::name string?) +(s/def ::path (s/nilable string?)) + +(s/def ::container + ;; (s/keys :req-un [::id ::name ::ctst/objects] + (s/keys :req-un [::id ::name] + :opt-un [::type ::path])) + +(defn make-container + [page-or-component type] + (assoc page-or-component :type type)) + +(defn page? + [container] + (= (:type container) :page)) + +(defn component? + [container] + (= (:type container) :component)) + +(defn get-container + [file type id] + (us/assert map? file) + (us/assert ::type type) + (us/assert uuid? id) + + (-> (if (= type :page) + (get-in file [:pages-index id]) + (get-in file [:components id])) + (assoc :type type))) + +(defn get-shape + [container shape-id] + (us/assert ::container container) + (us/assert ::us/uuid shape-id) + (-> container + (get :objects) + (get shape-id))) + +(defn shapes-seq + [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 + to the new ones." + [shape objects file-id components-v2] + (assert (nil? (:component-id shape))) + (assert (nil? (:component-file shape))) + (assert (nil? (:shape-ref shape))) + (let [;; Ensure that the component root is not an instance and + ;; it's no longer tied to a frame. + update-new-shape (fn [new-shape _original-shape] + (cond-> new-shape + true + (-> (assoc :frame-id nil) + (dissoc :component-root?)) + + (nil? (:parent-id new-shape)) + (dissoc :component-id + :component-file + :shape-ref))) + + ;; Make the original shape an instance of the new component. + ;; If one of the original shape children already was a component + ;; instance, maintain this instanceness untouched. + update-original-shape (fn [original-shape new-shape] + (cond-> original-shape + (nil? (:shape-ref original-shape)) + (-> (assoc :shape-ref (:id new-shape)) + (dissoc :touched)) + + (nil? (:parent-id new-shape)) + (assoc :component-id (:id new-shape) + :component-file file-id + :component-root? true) + + (and (nil? (:parent-id new-shape)) components-v2) + (assoc :main-instance? true) + + (some? (:parent-id new-shape)) + (dissoc :component-root?)))] + + (ctst/clone-object shape nil objects update-new-shape update-original-shape))) + +(defn make-component-instance + "Clone the shapes of the component, generating new names and ids, and linking + each new shape to the corresponding one of the component. Place the new instance + coordinates in the given position." + [container component component-file-id position main-instance?] + (let [component-shape (get-shape component (:id component)) + + orig-pos (gpt/point (:x component-shape) (:y component-shape)) + delta (gpt/subtract position orig-pos) + + objects (:objects container) + unames (volatile! (ctst/retrieve-used-names objects)) + + frame-id (ctst/frame-id-by-position objects (gpt/add orig-pos delta)) + + update-new-shape + (fn [new-shape original-shape] + (let [new-name (ctst/generate-unique-name @unames (:name new-shape))] + + (when (nil? (:parent-id original-shape)) + (vswap! unames conj new-name)) + + (cond-> new-shape + true + (as-> $ + (gsh/move $ delta) + (assoc $ :frame-id frame-id) + (assoc $ :parent-id + (or (:parent-id $) (:frame-id $))) + (dissoc $ :touched)) + + (nil? (:shape-ref original-shape)) + (assoc :shape-ref (:id original-shape)) + + (nil? (:parent-id original-shape)) + (assoc :component-id (:id original-shape) + :component-file component-file-id + :component-root? true + :name new-name) + + (and (nil? (:parent-id original-shape)) main-instance?) + (assoc :main-instance? true) + + (some? (:parent-id original-shape)) + (dissoc :component-root?)))) + + [new-shape new-shapes _] + (ctst/clone-object component-shape + nil + (get component :objects) + update-new-shape)] + + [new-shape new-shapes])) + diff --git a/common/src/app/common/types/file.cljc b/common/src/app/common/types/file.cljc index 5c7475d60..7653d9954 100644 --- a/common/src/app/common/types/file.cljc +++ b/common/src/app/common/types/file.cljc @@ -6,10 +6,27 @@ (ns app.common.types.file (:require - [app.common.spec :as us] - [app.common.types.color :as ctc] - [app.common.types.page :as ctp] - [clojure.spec.alpha :as s])) + [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.colors-list :as ctcl] + [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.types.typographies-list :as ctyl] + [app.common.types.typography :as cty] + [app.common.uuid :as uuid] + [clojure.spec.alpha :as s] + [cuerdas.core :as str])) + +;; Specs (s/def :internal.media-object/name string?) (s/def :internal.media-object/width ::us/safe-integer) @@ -57,3 +74,413 @@ ::recent-colors ::typographies ::media])) + +;; Initialization + +(def empty-file-data + {:version file-version + :pages [] + :pages-index {}}) + +(defn make-file-data + ([file-id components-v2] + (make-file-data file-id (uuid/next) components-v2)) + + ([file-id page-id components-v2] + (let [page (ctp/make-empty-page page-id "Page-1")] + (cond-> (-> empty-file-data + (assoc :id file-id) + (ctpl/add-page page)) + + components-v2 + (assoc-in [:options :components-v2] true))))) + +;; Helpers + +(defn file-data + [file] + (:data file)) + +(defn update-file-data + [file f] + (update file :data f)) + +(defn containers-seq + "Generate a sequence of all pages and all components, wrapped as containers" + [file-data] + (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))) + +;; Asset helpers + +(defmulti uses-asset? + "Checks if a shape uses the given asset." + (fn [asset-type _ _ _] asset-type)) + +(defmethod uses-asset? :component + [_ shape library-id component] + (ctk/instance-of? shape library-id (:id component))) + +(defmethod uses-asset? :color + [_ shape library-id color] + (ctc/uses-library-color? shape library-id (:id color))) + +(defmethod uses-asset? :typography + [_ shape library-id typography] + (cty/uses-library-typography? shape library-id (:id typography))) + +(defn find-asset-type-usages + "Find all usages of an asset in a file (may be in pages or in the components + of the local library). + + Returns a list ((asset ((container shapes) (container shapes)...))...)" + [file-data library-data asset-type] + (let [assets-seq (case asset-type + :component (ctkl/components-seq library-data) + :color (ctcl/colors-seq library-data) + :typography (ctyl/typographies-seq library-data)) + + find-usages-in-container + (fn [container asset] + (let [instances (filter #(uses-asset? asset-type % (:id library-data) asset) + (ctn/shapes-seq container))] + (when (d/not-empty? instances) + [[container instances]]))) + + find-asset-usages + (fn [file-data asset] + (mapcat #(find-usages-in-container % asset) (containers-seq file-data)))] + + (mapcat (fn [asset] + (let [instances (find-asset-usages file-data asset)] + (when (d/not-empty? instances) + [[asset instances]]))) + assets-seq))) + +(defn get-or-add-library-page + "If exists a page named 'Library backup', get the id and calculate the position to start + adding new components. If not, create it and start at (0, 0)." + [file-data grid-gap] + (let [library-page (d/seek #(= (:name %) "Library backup") (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 backup")] + [(ctpl/add-page file-data library-page) (:id library-page) (gpt/point 0 0)])))) + +(defn migrate-to-components-v2 + "If there is any component in the file library, add a new 'Library backup' and generate + main instances for all components there. Mark the file with the :comonents-v2 option." + [file-data] + (let [components (ctkl/components-seq file-data)] + (if (or (empty? components) + (get-in file-data [:options :components-v2])) + (assoc-in file-data [:options :components-v2] true) + (let [grid-gap 50 + + [file-data page-id start-pos] + (get-or-add-library-page file-data grid-gap) + + add-main-instance + (fn [file-data component position] + (let [page (ctpl/get-page file-data page-id) + + [new-shape new-shapes] + (ctn/make-component-instance page + component + (:id file-data) + position + true) + + 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))] + + (-> file-data + (ctpl/update-page page-id add-shapes) + (ctkl/update-component (:id component) update-component)))) + + add-instance-grid + (fn [file-data components] + (let [position-seq (ctst/generate-shape-grid + (map ctk/get-component-root components) + start-pos + grid-gap)] + (loop [file-data file-data + components-seq (seq components) + position-seq position-seq] + (let [component (first components-seq) + position (first position-seq)] + (if (nil? component) + file-data + (recur (add-main-instance file-data component position) + (rest components-seq) + (rest position-seq)))))))] + + (-> file-data + (add-instance-grid (sort-by :name components)) + (assoc-in [:options :components-v2] true)))))) + +(defn- absorb-components + [file-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/make-component-instance page + component + (:id file-data) + position + true) + + ; 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 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 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 [used-components (find-asset-type-usages file-data library-data :component) + used-colors (find-asset-type-usages file-data library-data :color) + used-typographies (find-asset-type-usages file-data library-data :typography)] + + (cond-> file-data + (d/not-empty? used-components) + (absorb-components used-components) + + (d/not-empty? used-colors) + (absorb-colors used-colors) + + (d/not-empty? used-typographies) + (absorb-typographies used-typographies)))) + + +;; Debug helpers + +(defn dump-tree + ([file-data page-id libraries] + (dump-tree file-data page-id libraries false false)) + + ([file-data page-id libraries show-ids] + (dump-tree file-data page-id libraries show-ids false)) + + ([file-data page-id libraries show-ids show-touched] + (let [page (ctpl/get-page file-data page-id) + objects (:objects page) + components (:components file-data) + root (d/seek #(nil? (:parent-id %)) (vals objects))] + + (letfn [(show-shape [shape-id level objects] + (let [shape (get objects shape-id)] + (println (str/pad (str (str/repeat " " level) + (:name shape) + (when (seq (:touched shape)) "*") + (when show-ids (str/format " <%s>" (:id shape)))) + {:length 20 + :type :right}) + (show-component shape objects)) + (when show-touched + (when (seq (:touched shape)) + (println (str (str/repeat " " level) + " " + (str (:touched shape))))) + (when (:remote-synced? shape) + (println (str (str/repeat " " level) + " (remote-synced)")))) + (when (:shapes shape) + (dorun (for [shape-id (:shapes shape)] + (show-shape shape-id (inc level) objects)))))) + + (show-component [shape objects] + (if (nil? (:shape-ref shape)) + "" + (let [root-shape (cph/get-component-shape objects shape) + component-id (when root-shape (:component-id root-shape)) + component-file-id (when root-shape (:component-file root-shape)) + component-file (when component-file-id (get libraries component-file-id nil)) + component (when component-id + (if component-file + (get-in component-file [:data :components component-id]) + (get components component-id))) + component-shape (when (and component (:shape-ref shape)) + (get-in component [:objects (:shape-ref shape)]))] + (str/format " %s--> %s%s%s" + (cond (:component-root? shape) "#" + (:component-id shape) "@" + :else "-") + (when component-file (str/format "<%s> " (:name component-file))) + (or (:name component-shape) "?") + (if (or (:component-root? shape) + (nil? (:component-id shape)) + true) + "" + (let [component-id (:component-id shape) + component-file-id (:component-file shape) + component-file (when component-file-id (get libraries component-file-id nil)) + component (if component-file + (get-in component-file [:data :components component-id]) + (get components component-id))] + (str/format " (%s%s)" + (when component-file (str/format "<%s> " (:name component-file))) + (:name component))))))))] + + (println "[Page]") + (show-shape (:id root) 0 objects) + + (dorun (for [component (vals components)] + (do + (println) + (println (str/format "[%s]" (:name component)) + (when show-ids + (str/format " (main: %s/%s)" + (:main-instance-page component) + (:main-instance-id component)))) + (show-shape (:id component) 0 (:objects component))))))))) + diff --git a/common/src/app/common/types/page.cljc b/common/src/app/common/types/page.cljc index 0ed513dc0..2aee3761a 100644 --- a/common/src/app/common/types/page.cljc +++ b/common/src/app/common/types/page.cljc @@ -9,6 +9,7 @@ [app.common.data :as d] [app.common.spec :as us] [app.common.types.shape :as cts] + [app.common.uuid :as uuid] [clojure.spec.alpha :as s])) ;; --- Grid options @@ -95,11 +96,22 @@ (s/def ::page (s/keys :req-un [::id ::name ::objects ::options])) -(s/def ::type #{:page :component}) -(s/def ::path (s/nilable string?)) -(s/def ::container - (s/keys :req-un [::id ::name ::objects] - :opt-un [::type ::path])) +;; --- Initialization + +(def root uuid/zero) + +(def empty-page-data + {:options {} + :objects {root + {:id root + :type :frame + :name "Root Frame"}}}) + +(defn make-empty-page + [id name] + (assoc empty-page-data + :id id + :name name)) ;; --- Helpers for flow diff --git a/common/src/app/common/types/pages_list.cljc b/common/src/app/common/types/pages_list.cljc new file mode 100644 index 000000000..5275e480e --- /dev/null +++ b/common/src/app/common/types/pages_list.cljc @@ -0,0 +1,34 @@ +;; 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.pages-list + (:require + [app.common.data :as d])) + +(defn get-page + [file-data id] + (get-in file-data [:pages-index id])) + +(defn add-page + [file-data page] + (let [; It's legitimate to add a page that is already there, + ; for example in an idempotent changes operation. + conj-if-not-exists (fn [pages id] + (cond-> pages + (not (d/seek #(= % id) pages)) + (conj id)))] + (-> file-data + (update :pages conj-if-not-exists (:id page)) + (update :pages-index assoc (:id page) page)))) + +(defn pages-seq + [file-data] + (vals (:pages-index file-data))) + +(defn update-page + [file-data page-id f] + (update-in file-data [:pages-index page-id] f)) + diff --git a/common/src/app/common/types/shape.cljc b/common/src/app/common/types/shape.cljc index a5b182f64..249c589ce 100644 --- a/common/src/app/common/types/shape.cljc +++ b/common/src/app/common/types/shape.cljc @@ -6,8 +6,13 @@ (ns app.common.types.shape (:require + [app.common.colors :as clr] + [app.common.data :as d] + [app.common.exceptions :as ex] [app.common.geom.matrix :as gmt] [app.common.geom.point :as gpt] + [app.common.geom.shapes :as gsh] + [app.common.pages.common :refer [default-color]] [app.common.spec :as us] [app.common.types.color :as ctc] [app.common.types.shape.blur :as ctsb] @@ -16,6 +21,7 @@ [app.common.types.shape.layout :as ctsl] [app.common.types.shape.radius :as ctsr] [app.common.types.shape.shadow :as ctss] + [app.common.uuid :as uuid] [clojure.set :as set] [clojure.spec.alpha :as s])) @@ -316,3 +322,155 @@ (s/and (s/multi-spec shape-spec :type) #(contains? % :type) #(contains? % :name))) + + +;; --- Initialization + +(def default-shape-attrs + {}) + +(def default-frame-attrs + {:frame-id uuid/zero + :fills [{:fill-color clr/white + :fill-opacity 1}] + :strokes [] + :shapes [] + :hide-fill-on-export false}) + +(def ^:private minimal-shapes + [{:type :rect + :name "Rect-1" + :fills [{:fill-color default-color + :fill-opacity 1}] + :strokes [] + :rx 0 + :ry 0} + + {:type :image + :rx 0 + :ry 0 + :fills [] + :strokes []} + + {:type :circle + :name "Circle-1" + :fills [{:fill-color default-color + :fill-opacity 1}] + :strokes []} + + {:type :path + :name "Path-1" + :fills [] + :strokes [{:stroke-style :solid + :stroke-alignment :center + :stroke-width 2 + :stroke-color clr/black + :stroke-opacity 1}]} + + {:type :frame + :name "Board-1" + :fills [{:fill-color clr/white + :fill-opacity 1}] + :strokes [] + :stroke-style :none + :stroke-alignment :center + :stroke-width 0 + :stroke-color clr/black + :stroke-opacity 0 + :rx 0 + :ry 0} + + {:type :text + :name "Text-1" + :content nil} + + {:type :svg-raw}]) + +(def empty-selrect + {:x 0 :y 0 + :x1 0 :y1 0 + :x2 0.01 :y2 0.01 + :width 0.01 :height 0.01}) + +(defn make-minimal-shape + [type] + (let [type (cond (= type :curve) :path + :else type) + shape (d/seek #(= type (:type %)) minimal-shapes)] + (when-not shape + (ex/raise :type :assertion + :code :shape-type-not-implemented + :context {:type type})) + + (cond-> shape + :always + (assoc :id (uuid/next)) + + (not= :path (:type shape)) + (assoc :x 0 + :y 0 + :width 0.01 + :height 0.01 + :selrect {:x 0 + :y 0 + :x1 0 + :y1 0 + :x2 0.01 + :y2 0.01 + :width 0.01 + :height 0.01})))) + +(defn make-minimal-group + [frame-id rect group-name] + {:id (uuid/next) + :type :group + :name group-name + :shapes [] + :frame-id frame-id + :x (:x rect) + :y (:y rect) + :width (:width rect) + :height (:height rect)}) + +(defn setup-rect-selrect + "Initializes the selrect and points for a shape." + [shape] + (let [selrect (gsh/rect->selrect shape) + points (gsh/rect->points shape)] + (-> shape + (assoc :selrect selrect + :points points)))) + +(defn- setup-rect + "A specialized function for setup rect-like shapes." + [shape {:keys [x y width height]}] + (-> shape + (assoc :x x :y y :width width :height height) + (setup-rect-selrect))) + +(defn- setup-image + [{:keys [metadata] :as shape} props] + (-> (setup-rect shape props) + (assoc + :proportion (/ (:width metadata) + (:height metadata)) + :proportion-lock true))) + +(defn setup-shape + "A function that initializes the geometric data of + the shape. The props must have :x :y :width :height." + ([props] + (setup-shape {:type :rect} props)) + + ([shape props] + (case (:type shape) + :image (setup-image shape props) + (setup-rect shape props)))) + +(defn make-shape + "Make a non group shape, ready to use." + [type geom-props attrs] + (-> (make-minimal-shape type) + (setup-shape geom-props) + (merge attrs))) + diff --git a/common/src/app/common/types/shape_tree.cljc b/common/src/app/common/types/shape_tree.cljc new file mode 100644 index 000000000..b03054e24 --- /dev/null +++ b/common/src/app/common/types/shape_tree.cljc @@ -0,0 +1,350 @@ +;; 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.shape-tree + (:require + [app.common.data :as d] + [app.common.data.macros :as dm] + [app.common.geom.point :as gpt] + [app.common.geom.shapes :as gsh] + [app.common.math :as mth] + [app.common.pages.helpers :as cph] + [app.common.spec :as us] + [app.common.types.shape :as cts] + [app.common.uuid :as uuid] + [clojure.spec.alpha :as s])) + +(s/def ::objects (s/map-of uuid? ::cts/shape)) + +(defn add-shape + "Insert a shape in the tree, at the given index below the given parent or frame. + Update the parent as needed." + [id shape container frame-id parent-id index ignore-touched] + (let [update-parent-shapes + (fn [shapes] + ;; Ensure that shapes is always a vector. + (let [shapes (into [] shapes)] + (cond + (some #{id} shapes) + shapes + + (nil? index) + (conj shapes id) + + :else + (cph/insert-at-index shapes index [id])))) + + update-parent + (fn [parent] + (-> parent + (update :shapes update-parent-shapes) + (update :shapes d/vec-without-nils) + (cond-> (and (:shape-ref parent) + (not= (:id parent) frame-id) + (not ignore-touched)) + (-> (update :touched cph/set-touched-group :shapes-group) + (dissoc :remote-synced?))))) + + ;; TODO: this looks wrong, why we allow nil values? + update-objects + (fn [objects parent-id] + (if (and (or (nil? parent-id) (contains? objects parent-id)) + (or (nil? frame-id) (contains? objects frame-id))) + (-> objects + (assoc id (-> shape + (assoc :frame-id frame-id) + (assoc :parent-id parent-id) + (assoc :id id))) + (update parent-id update-parent)) + objects)) + + parent-id (or parent-id frame-id)] + + (update container :objects update-objects parent-id))) + +(defn set-shape + "Replace a shape in the tree with a new one" + [container shape] + (assoc-in container [:objects (:id shape)] shape)) + +(defn get-frames + "Retrieves all frame objects as vector" + [objects] + (or (-> objects meta ::index-frames) + (let [lookup (d/getf objects) + xform (comp (remove #(= uuid/zero %)) + (keep lookup) + (filter cph/frame-shape?))] + (->> (keys objects) + (into [] xform))))) + +(defn get-frames-ids + "Retrieves all frame ids as vector" + [objects] + (->> (get-frames objects) + (mapv :id))) + +(defn get-nested-frames + [objects frame-id] + (into #{} + (comp (filter cph/frame-shape?) + (map :id)) + (cph/get-children objects frame-id))) + +(defn get-root-frames-ids + "Retrieves all frame objects as vector. It is not implemented in + function of `get-immediate-children` for performance reasons. This + function is executed in the render hot path." + [objects] + (let [add-frame + (fn [result shape] + (cond-> result + (cph/frame-shape? shape) + (conj (:id shape))))] + (cph/reduce-objects objects (complement cph/frame-shape?) add-frame []))) + +(defn get-root-objects + "Get all the objects under the root object" + [objects] + (let [add-shape + (fn [result shape] + (conj result shape))] + (cph/reduce-objects objects (complement cph/frame-shape?) add-shape []))) + +(defn get-root-shapes + "Get all shapes that are not frames" + [objects] + (let [add-shape + (fn [result shape] + (cond-> result + (not (cph/frame-shape? shape)) + (conj shape)))] + (cph/reduce-objects objects (complement cph/frame-shape?) add-shape []))) + +(defn get-root-shapes-ids + [objects] + (->> (get-root-shapes objects) + (mapv :id))) + +(defn get-base + [objects id-a id-b] + + (let [parents-a (reverse (cph/get-parents-seq objects id-a)) + parents-b (reverse (cph/get-parents-seq objects id-b)) + + [base base-child-a base-child-b] + (loop [parents-a (rest parents-a) + parents-b (rest parents-b) + base uuid/zero] + (cond + (not= (first parents-a) (first parents-b)) + [base (first parents-a) (first parents-b)] + + (or (empty? parents-a) (empty? parents-b)) + [uuid/zero (first parents-a) (first parents-b)] + + :else + (recur (rest parents-a) (rest parents-b) (first parents-a)))) + + index-base-a (when base-child-a (cph/get-position-on-parent objects base-child-a)) + index-base-b (when base-child-b (cph/get-position-on-parent objects base-child-b))] + + [base index-base-a index-base-b])) + +(defn is-shape-over-shape? + [objects base-shape-id over-shape-id {:keys [top-frames?]}] + + (let [[base index-a index-b] (get-base objects base-shape-id over-shape-id)] + (cond + (= base base-shape-id) + (and (not top-frames?) + (cph/frame-shape? objects base-shape-id) + (cph/root-frame? objects base-shape-id)) + + (= base over-shape-id) + (or top-frames? + (not (cph/frame-shape? objects over-shape-id)) + (not (cph/root-frame? objects over-shape-id))) + + :else + (< index-a index-b)))) + +(defn sort-z-index + ([objects ids] + (sort-z-index objects ids nil)) + + ([objects ids {:keys [bottom-frames?] :as options}] + (letfn [(comp [id-a id-b] + (let [type-a (dm/get-in objects [id-a :type]) + type-b (dm/get-in objects [id-b :type])] + (cond + (and bottom-frames? (= :frame type-a) (not= :frame type-b)) + 1 + + (and bottom-frames? (not= :frame type-a) (= :frame type-b)) + -1 + + (= id-a id-b) + 0 + + (is-shape-over-shape? objects id-a id-b options) + 1 + + :else + -1)))] + (sort comp ids)))) + +(defn frame-id-by-position + [objects position] + (assert (gpt/point? position)) + (let [top-frame + (->> (get-frames-ids objects) + (sort-z-index objects) + (d/seek #(and position (gsh/has-point? (get objects %) position))))] + (or top-frame uuid/zero))) + +(defn frame-by-position + [objects position] + (let [frame-id (frame-id-by-position objects position)] + (get objects frame-id))) + +(defn get-viewer-frames + ([objects] + (get-viewer-frames objects nil)) + + ([objects {:keys [all-frames?]}] + (into [] + (comp (map (d/getf objects)) + (if all-frames? + identity + (remove :hide-in-viewer))) + (sort-z-index objects (get-frames-ids objects) {:top-frames? true})))) + +(defn start-page-index + [objects] + (with-meta objects {::index-frames (get-frames (with-meta objects nil))})) + +(defn update-page-index + [objects] + (with-meta objects {::index-frames (get-frames (with-meta objects nil))})) + +(defn start-object-indices + [file] + (letfn [(process-index [page-index page-id] + (update-in page-index [page-id :objects] start-page-index))] + (update file :pages-index #(reduce process-index % (keys %))))) + +(defn update-object-indices + [file page-id] + (update-in file [:pages-index page-id :objects] update-page-index)) + +(defn rotated-frame? + [frame] + (not (mth/almost-zero? (:rotation frame 0)))) + +(defn retrieve-used-names + [objects] + (into #{} (comp (map :name) (remove nil?)) (vals objects))) + +(defn- extract-numeric-suffix + [basename] + (if-let [[_ p1 p2] (re-find #"(.*)-([0-9]+)$" basename)] + [p1 (+ 1 (d/parse-integer p2))] + [basename 1])) + +(defn generate-unique-name + "A unique name generator" + [used basename] + (s/assert ::us/set-of-string used) + (s/assert ::us/string basename) + (if-not (contains? used basename) + basename + (let [[prefix initial] (extract-numeric-suffix basename)] + (loop [counter initial] + (let [candidate (str prefix "-" counter)] + (if (contains? used candidate) + (recur (inc counter)) + candidate)))))) + +(defn clone-object + "Gets a copy of the object and all its children, with new ids + and with the parent-children links correctly set. Admits functions + to make more transformations to the cloned objects and the + original ones. + + Returns the cloned object, the list of all new objects (including + the cloned one), and possibly a list of original objects modified. + + The list of objects are returned in tree traversal order, respecting + the order of the children of each parent." + + ([object parent-id objects update-new-object] + (clone-object object parent-id objects update-new-object (fn [object _] object))) + + ([object parent-id objects update-new-object update-original-object] + (let [new-id (uuid/next)] + (loop [child-ids (seq (:shapes object)) + new-direct-children [] + new-children [] + updated-children []] + + (if (empty? child-ids) + (let [new-object (cond-> object + true + (assoc :id new-id + :parent-id parent-id) + + (some? (:shapes object)) + (assoc :shapes (mapv :id new-direct-children))) + + new-object (update-new-object new-object object) + new-objects (into [new-object] new-children) + + updated-object (update-original-object object new-object) + updated-objects (if (identical? object updated-object) + updated-children + (into [updated-object] updated-children))] + + [new-object new-objects updated-objects]) + + (let [child-id (first child-ids) + child (get objects child-id) + _ (us/assert some? child) + + [new-child new-child-objects updated-child-objects] + (clone-object child new-id objects update-new-object update-original-object)] + + (recur + (next child-ids) + (into new-direct-children [new-child]) + (into new-children new-child-objects) + (into updated-children updated-child-objects)))))))) + +(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 start-pos gap] + (let [shapes-bounds (map gsh/bounding-box shapes) + + grid-size (mth/ceil (mth/sqrt (count shapes))) + row-size (+ (apply max (map :height shapes-bounds)) + gap) + column-size (+ (apply max (map :width shapes-bounds)) + gap) + + next-pos (fn [position] + (let [counter (inc (:counter (meta position))) + row (quot counter grid-size) + column (mod counter grid-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 start-pos + {:counter 0})))) + diff --git a/common/src/app/common/types/typographies_list.cljc b/common/src/app/common/types/typographies_list.cljc new file mode 100644 index 000000000..1e7dace0d --- /dev/null +++ b/common/src/app/common/types/typographies_list.cljc @@ -0,0 +1,24 @@ +;; 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.typographies-list) + +(defn typographies-seq + [file-data] + (vals (:typographies file-data))) + +(defn add-typography + [file-data typography] + (update file-data :typographies assoc (:id typography) typography)) + +(defn get-typography + [file-data typography-id] + (get-in file-data [:typographies typography-id])) + +(defn update-typography + [file-data typography-id f] + (update-in file-data [:typographies typography-id] f)) + diff --git a/common/src/app/common/types/typography.cljc b/common/src/app/common/types/typography.cljc index ff63bf14b..0c1a15ef7 100644 --- a/common/src/app/common/types/typography.cljc +++ b/common/src/app/common/types/typography.cljc @@ -6,7 +6,8 @@ (ns app.common.types.typography (:require - [clojure.spec.alpha :as s])) + [app.common.text :as txt] + [clojure.spec.alpha :as s])) (s/def ::id uuid?) (s/def ::name string?) @@ -35,4 +36,37 @@ ::text-transform] :opt-un [::path])) +(defn uses-library-typographies? + "Check if the shape uses any typography in the given library." + [shape library-id] + (and (= (:type shape) :text) + (->> shape + :content + ;; Check if any node in the content has a reference for the library + (txt/node-seq + #(and (some? (:typography-ref-id %)) + (= (:typography-ref-file %) library-id)))))) + +(defn uses-library-typography? + "Check if the shape uses the given library typography." + [shape library-id typography-id] + (and (= (:type shape) :text) + (->> shape + :content + ;; Check if any node in the content has a reference for the library + (txt/node-seq + #(and (= (:typography-ref-id %) typography-id) + (= (:typography-ref-file %) library-id)))))) + +(defn remap-typographies + "Change the shape so that any use of the given typography now points to + the given library." + [shape library-id typography] + (let [remap-typography #(assoc % :typography-ref-file library-id)] + + (update shape :content + (fn [content] + (txt/transform-nodes #(= (:typography-ref-id %) (:id typography)) + remap-typography + content))))) diff --git a/common/test/app/common/geom_shapes_test.cljc b/common/test/app/common/geom_shapes_test.cljc index 079ecafd6..b7307b3c1 100644 --- a/common/test/app/common/geom_shapes_test.cljc +++ b/common/test/app/common/geom_shapes_test.cljc @@ -10,7 +10,7 @@ [app.common.geom.point :as gpt] [app.common.geom.shapes :as gsh] [app.common.math :as mth :refer [close?]] - [app.common.pages :refer [make-minimal-shape]] + [app.common.types.shape :as cts] [clojure.test :as t])) (def default-path @@ -41,7 +41,7 @@ (defn create-test-shape ([type] (create-test-shape type {})) ([type params] - (-> (make-minimal-shape type) + (-> (cts/make-minimal-shape type) (merge params) (cond-> (= type :path) (add-path-data) diff --git a/common/test/app/common/pages_test.cljc b/common/test/app/common/pages_test.cljc index abfa5f8f3..89264a93d 100644 --- a/common/test/app/common/pages_test.cljc +++ b/common/test/app/common/pages_test.cljc @@ -9,12 +9,13 @@ [clojure.test :as t] [clojure.pprint :refer [pprint]] [app.common.pages :as cp] + [app.common.types.file :as ctf] [app.common.uuid :as uuid])) (t/deftest process-change-set-option (let [file-id (uuid/custom 2 2) page-id (uuid/custom 1 1) - data (cp/make-file-data file-id page-id)] + data (ctf/make-file-data file-id page-id true)] (t/testing "Sets option single" (let [chg {:type :set-option :page-id page-id @@ -80,7 +81,7 @@ (t/deftest process-change-add-obj (let [file-id (uuid/custom 2 2) page-id (uuid/custom 1 1) - data (cp/make-file-data file-id page-id) + data (ctf/make-file-data file-id page-id true) id-a (uuid/custom 2 1) id-b (uuid/custom 2 2) id-c (uuid/custom 2 3)] @@ -134,7 +135,7 @@ (t/deftest process-change-mod-obj (let [file-id (uuid/custom 2 2) page-id (uuid/custom 1 1) - data (cp/make-file-data file-id page-id)] + data (ctf/make-file-data file-id page-id true)] (t/testing "simple mod-obj" (let [chg {:type :mod-obj :page-id page-id @@ -161,7 +162,7 @@ (let [file-id (uuid/custom 2 2) page-id (uuid/custom 1 1) id (uuid/custom 2 1) - data (cp/make-file-data file-id page-id) + data (ctf/make-file-data file-id page-id true) data (-> data (assoc-in [:pages-index page-id :objects uuid/zero :shapes] [id]) (assoc-in [:pages-index page-id :objects id] @@ -205,7 +206,7 @@ file-id (uuid/custom 2 2) page-id (uuid/custom 1 1) - data (cp/make-file-data file-id page-id) + data (ctf/make-file-data file-id page-id true) data (update-in data [:pages-index page-id :objects] #(-> % @@ -449,7 +450,7 @@ :obj {:type :rect :name "Shape 3"}} ] - data (cp/make-file-data file-id page-id) + data (ctf/make-file-data file-id page-id true) data (cp/process-changes data changes)] (t/testing "preserve order on multiple shape mov 1" @@ -556,7 +557,7 @@ :parent-id group-1-id :shapes [shape-1-id shape-2-id]}] - data (cp/make-file-data file-id page-id) + data (ctf/make-file-data file-id page-id true) data (cp/process-changes data changes)] (t/testing "case 1" diff --git a/common/test/app/common/test_helpers/components.cljc b/common/test/app/common/test_helpers/components.cljc new file mode 100644 index 000000000..843b74a4e --- /dev/null +++ b/common/test/app/common/test_helpers/components.cljc @@ -0,0 +1,149 @@ +(ns app.common.test-helpers.components + (:require + [clojure.test :as t] + [app.common.pages.helpers :as cph] + [app.common.types.component :as ctk] + [app.common.types.container :as ctn])) + +;; ---- Helpers to manage libraries and synchronization + +(defn check-instance-root + [shape] + (t/is (some? (:shape-ref shape))) + (t/is (some? (:component-id shape))) + (t/is (= (:component-root? shape) true))) + +(defn check-instance-subroot + [shape] + (t/is (some? (:shape-ref shape))) + (t/is (some? (:component-id shape))) + (t/is (nil? (:component-root? shape)))) + +(defn check-instance-child + [shape] + (t/is (some? (:shape-ref shape))) + (t/is (nil? (:component-id shape))) + (t/is (nil? (:component-file shape))) + (t/is (nil? (:component-root? shape)))) + +(defn check-instance-inner + [shape] + (if (some? (:component-id shape)) + (check-instance-subroot shape) + (check-instance-child shape))) + +(defn check-noninstance + [shape] + (t/is (nil? (:shape-ref shape))) + (t/is (nil? (:component-id shape))) + (t/is (nil? (:component-file shape))) + (t/is (nil? (:component-root? shape))) + (t/is (nil? (:remote-synced? shape))) + (t/is (nil? (:touched shape)))) + +(defn check-from-file + [shape file] + (t/is (= (:component-file shape) + (:id file)))) + +(defn resolve-instance + "Get the shape with the given id and all its children, and + verify that they are a well constructed instance tree." + [page root-inst-id] + (let [root-inst (ctn/get-shape page root-inst-id) + shapes-inst (cph/get-children-with-self (:objects page) + root-inst-id)] + (check-instance-root (first shapes-inst)) + (run! check-instance-inner (rest shapes-inst)) + + shapes-inst)) + +(defn resolve-noninstance + "Get the shape with the given id and all its children, and + verify that they are not a component instance." + [page root-inst-id] + (let [root-inst (ctn/get-shape page root-inst-id) + shapes-inst (cph/get-children-with-self (:objects page) + root-inst-id)] + (run! check-noninstance shapes-inst) + + shapes-inst)) + +(defn resolve-instance-and-main + "Get the shape with the given id and all its children, and also + the main component and all its shapes." + [page root-inst-id libraries] + (let [root-inst (ctn/get-shape page root-inst-id) + + component (cph/get-component libraries (:component-id root-inst)) + + shapes-inst (cph/get-children-with-self (:objects page) root-inst-id) + shapes-main (cph/get-children-with-self (:objects component) (:shape-ref root-inst)) + + unique-refs (into #{} (map :shape-ref) shapes-inst) + + main-exists? (fn [shape] + (let [component-shape + (cph/get-component-shape (:objects page) shape) + + component + (cph/get-component libraries (:component-id component-shape)) + + main-shape + (ctn/get-shape component (:shape-ref shape))] + + (t/is (some? main-shape))))] + + ;; Validate that the instance tree is well constructed + (check-instance-root (first shapes-inst)) + (run! check-instance-inner (rest shapes-inst)) + (t/is (= (count shapes-inst) + (count shapes-main) + (count unique-refs))) + (run! main-exists? shapes-inst) + + [shapes-inst shapes-main component])) + +(defn resolve-instance-and-main-allow-dangling + "Get the shape with the given id and all its children, and also + the main component and all its shapes. Allows shapes with the + corresponding component shape missing." + [page root-inst-id libraries] + (let [root-inst (ctn/get-shape page root-inst-id) + + component (cph/get-component libraries (:component-id root-inst)) + + shapes-inst (cph/get-children-with-self (:objects page) root-inst-id) + shapes-main (cph/get-children-with-self (:objects component) (:shape-ref root-inst)) + + unique-refs (into #{} (map :shape-ref) shapes-inst) + + main-exists? (fn [shape] + (let [component-shape + (cph/get-component-shape (:objects page) shape) + + component + (cph/get-component libraries (:component-id component-shape)) + + main-shape + (ctn/get-shape component (:shape-ref shape))] + + (t/is (some? main-shape))))] + + ;; Validate that the instance tree is well constructed + (check-instance-root (first shapes-inst)) + + [shapes-inst shapes-main component])) + +(defn resolve-component + "Get the component with the given id and all its shapes." + [page component-id libraries] + (let [component (cph/get-component libraries component-id) + root-main (ctk/get-component-root component) + shapes-main (cph/get-children-with-self (:objects component) (:id root-main))] + + ;; Validate that the component tree is well constructed + (run! check-noninstance shapes-main) + + [shapes-main component])) + diff --git a/common/test/app/common/test_helpers/files.cljc b/common/test/app/common/test_helpers/files.cljc new file mode 100644 index 000000000..85d4b980d --- /dev/null +++ b/common/test/app/common/test_helpers/files.cljc @@ -0,0 +1,149 @@ +;; 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.test-helpers.files + (:require + [app.common.geom.point :as gpt] + [app.common.types.components-list :as ctkl] + [app.common.types.colors-list :as ctcl] + [app.common.types.container :as ctn] + [app.common.types.file :as ctf] + [app.common.types.pages-list :as ctpl] + [app.common.types.shape :as cts] + [app.common.types.shape-tree :as ctst] + [app.common.types.typographies-list :as ctyl] + [app.common.uuid :as uuid])) + +(def ^:private idmap (atom {})) + +(defn reset-idmap! [] + (reset! idmap {})) + +(defn id + [label] + (get @idmap label)) + +(defn sample-file + ([file-id page-id] (sample-file file-id page-id nil)) + ([file-id page-id props] + (merge {:id file-id + :name (get props :name "File1") + :data (ctf/make-file-data file-id page-id true)} + props))) + +(defn sample-shape + [file label type page-id props] + (ctf/update-file-data + file + (fn [file-data] + (let [frame-id (get props :frame-id uuid/zero) + parent-id (get props :parent-id uuid/zero) + shape (if (= type :group) + (cts/make-minimal-group frame-id + {:x 0 :y 0 :width 1 :height 1} + (get props :name "Group1")) + (cts/make-shape type + {:x 0 :y 0 :width 1 :height 1} + props))] + + (swap! idmap assoc label (:id shape)) + (ctpl/update-page file-data + page-id + #(ctst/add-shape (:id shape) + shape + % + frame-id + parent-id + 0 + true)))))) + +(defn sample-component + [file label page-id shape-id] + (ctf/update-file-data + file + (fn [file-data] + (let [page (ctpl/get-page file-data page-id) + + [component-shape component-shapes updated-shapes] + (ctn/make-component-shape (ctn/get-shape page shape-id true) + (:objects page) + (:id file) + true)] + + (swap! idmap assoc label (:id component-shape)) + (-> file-data + (ctpl/update-page page-id + #(reduce (fn [page shape] (ctst/set-shape page shape)) + % + updated-shapes)) + (ctkl/add-component (:id component-shape) + (:name component-shape) + "" + shape-id + page-id + component-shapes)))))) + +(defn sample-instance + [file label page-id library component-id] + (ctf/update-file-data + file + (fn [file-data] + (let [[instance-shape instance-shapes] + (ctn/make-component-instance (ctpl/get-page file-data page-id) + (ctkl/get-component (:data library) component-id) + (:id library) + (gpt/point 0 0) + false)] + + (swap! idmap assoc label (:id instance-shape)) + (-> file-data + (ctpl/update-page page-id + #(reduce (fn [page shape] + (ctst/add-shape (:id shape) + shape + page + uuid/zero + (:parent-id shape) + 0 + true)) + % + instance-shapes))))))) + +(defn sample-color + [file label props] + (ctf/update-file-data + file + (fn [file-data] + (let [id (uuid/next) + props (merge {:id id + :name "Color-1" + :color "#000000" + :opacity 1} + props)] + (swap! idmap assoc label id) + (ctcl/add-color file-data props))))) + +(defn sample-typography + [file label props] + (ctf/update-file-data + file + (fn [file-data] + (let [id (uuid/next) + props (merge {:id id + :name "Typography-1" + :font-id "sourcesanspro" + :font-family "sourcesanspro" + :font-size "14" + :font-style "normal" + :font-variant-id "regular" + :font-weight "400" + :line-height "1.2" + :letter-spacing "0" + :text-transform "none"} + props)] + (swap! idmap assoc label id) + (ctyl/add-typography file-data props))))) + diff --git a/common/test/app/common/types/file_test.cljc b/common/test/app/common/types/file_test.cljc new file mode 100644 index 000000000..70f14a36a --- /dev/null +++ b/common/test/app/common/types/file_test.cljc @@ -0,0 +1,209 @@ +;; 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.file-test + (:require + ;; Uncomment to debug + ;; [clojure.pprint :refer [pprint]] + ;; [cuerdas.core :as str] + [clojure.test :as t] + [app.common.data :as d] + [app.common.geom.point :as gpt] + [app.common.text :as txt] + [app.common.types.colors-list :as ctcl] + [app.common.types.component :as ctk] + [app.common.types.components-list :as ctkl] + [app.common.types.container :as ctn] + [app.common.types.file :as ctf] + [app.common.types.pages-list :as ctpl] + [app.common.types.shape :as cts] + [app.common.types.shape-tree :as ctst] + [app.common.types.typographies-list :as ctyl] + [app.common.uuid :as uuid] + [app.common.test-helpers.files :as thf] + [app.common.test-helpers.components :as thk])) + +(t/use-fixtures :each + {:before thf/reset-idmap!}) + +(t/deftest test-absorb-components + (let [library-id (uuid/custom 1 1) + library-page-id (uuid/custom 2 2) + file-id (uuid/custom 3 3) + file-page-id (uuid/custom 4 4) + + library (-> (thf/sample-file library-id library-page-id {:is-shared true}) + (thf/sample-shape :group1 + :group + library-page-id + {:name "Group1"}) + (thf/sample-shape :shape1 + :rect + library-page-id + {:name "Rect1" + :parent-id (thf/id :group1)}) + (thf/sample-component :component1 + library-page-id + (thf/id :group1))) + + file (-> (thf/sample-file file-id file-page-id) + (thf/sample-instance :instance1 + file-page-id + library + (thf/id :component1))) + + absorbed-file (ctf/update-file-data + file + #(ctf/absorb-assets % (:data library))) + + pages (ctpl/pages-seq (ctf/file-data absorbed-file)) + components (ctkl/components-seq (ctf/file-data absorbed-file)) + shapes-1 (ctn/shapes-seq (first pages)) + shapes-2 (ctn/shapes-seq (second pages)) + + [[p-group p-shape] [c-group1 c-shape1] component1] + (thk/resolve-instance-and-main + (first pages) + (:id (second shapes-1)) + {file-id absorbed-file}) + + [[lp-group lp-shape] [c-group2 c-shape2] component2] + (thk/resolve-instance-and-main + (second pages) + (:id (second shapes-2)) + {file-id absorbed-file})] + + ;; Uncomment to debug + + ;; (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 library} + ;; true) + + ;; (println "\n===== absorbed file") + ;; (println (str "\n<" (:name (first pages)) ">")) + ;; (ctf/dump-tree (:data absorbed-file) + ;; (:id (first pages)) + ;; {file-id absorbed-file} + ;; false) + ;; (println (str "\n<" (:name (second pages)) ">")) + ;; (ctf/dump-tree (:data absorbed-file) + ;; (:id (second pages)) + ;; {file-id absorbed-file} + ;; false) + + (t/is (= (count pages) 2)) + (t/is (= (:name (first pages)) "Page-1")) + (t/is (= (:name (second pages)) "Library backup")) + + (t/is (= (count components) 1)) + + (t/is (= (:name p-group) "Group1")) + (t/is (ctk/instance-of? p-group file-id (:id component1))) + (t/is (not (:main-instance? p-group))) + (t/is (not (ctk/is-main-instance? (:id p-group) file-page-id component1))) + (t/is (ctk/is-main-of? c-group1 p-group)) + + (t/is (= (:name p-shape) "Rect1")) + (t/is (ctk/is-main-of? c-shape1 p-shape)))) + + +(t/deftest test-absorb-colors + (let [library-id (uuid/custom 1 1) + library-page-id (uuid/custom 2 2) + file-id (uuid/custom 3 3) + file-page-id (uuid/custom 4 4) + + library (-> (thf/sample-file library-id library-page-id {:is-shared true}) + (thf/sample-color :color1 {:name "Test color" + :color "#abcdef"})) + + file (-> (thf/sample-file file-id file-page-id) + (thf/sample-shape :shape1 + :rect + file-page-id + {:name "Rect1" + :fills [{:fill-color "#abcdef" + :fill-opacity 1 + :fill-color-ref-id (thf/id :color1) + :fill-color-ref-file library-id}]})) + + absorbed-file (ctf/update-file-data + file + #(ctf/absorb-assets % (:data library))) + + colors (ctcl/colors-seq (ctf/file-data absorbed-file)) + page (ctpl/get-page (ctf/file-data absorbed-file) file-page-id) + shape1 (ctn/get-shape page (thf/id :shape1)) + fill (first (:fills shape1))] + + (t/is (= (count colors) 1)) + (t/is (= (:id (first colors)) (thf/id :color1))) + (t/is (= (:name (first colors)) "Test color")) + (t/is (= (:color (first colors)) "#abcdef")) + + (t/is (= (:fill-color fill) "#abcdef")) + (t/is (= (:fill-color-ref-id fill) (thf/id :color1))) + (t/is (= (:fill-color-ref-file fill) file-id)))) + +(t/deftest test-absorb-typographies + (let [library-id (uuid/custom 1 1) + library-page-id (uuid/custom 2 2) + file-id (uuid/custom 3 3) + file-page-id (uuid/custom 4 4) + + library (-> (thf/sample-file library-id library-page-id {:is-shared true}) + (thf/sample-typography :typography1 {:name "Test typography"})) + + file (-> (thf/sample-file file-id file-page-id) + (thf/sample-shape :shape1 + :text + file-page-id + {:name "Text1" + :content {:type "root" + :children [{:type "paragraph-set" + :children [{:type "paragraph" + :key "67uep" + :children [{:text "Example text" + :typography-ref-id (thf/id :typography1) + :typography-ref-file library-id + :line-height "1.2" + :font-style "normal" + :text-transform "none" + :text-align "left" + :font-id "sourcesanspro" + :font-family "sourcesanspro" + :font-size "14" + :font-weight "400" + :font-variant-id "regular" + :text-decoration "none" + :letter-spacing "0" + :fills [{:fill-color "#000000" + :fill-opacity 1}]}] + }]}]}})) + absorbed-file (ctf/update-file-data + file + #(ctf/absorb-assets % (:data library))) + + typographies (ctyl/typographies-seq (ctf/file-data absorbed-file)) + page (ctpl/get-page (ctf/file-data absorbed-file) file-page-id) + shape1 (ctn/get-shape page (thf/id :shape1)) + text-node (d/seek #(some? (:text %)) (txt/node-seq (:content shape1)))] + + (t/is (= (count typographies) 1)) + (t/is (= (:id (first typographies)) (thf/id :typography1))) + (t/is (= (:name (first typographies)) "Test typography")) + + (t/is (= (:typography-ref-id text-node) (thf/id :typography1))) + (t/is (= (:typography-ref-file text-node) file-id)))) + diff --git a/common/test/app/common/spec_interactions_test.cljc b/common/test/app/common/types/shape/spec_interactions_test.cljc similarity index 97% rename from common/test/app/common/spec_interactions_test.cljc rename to common/test/app/common/types/shape/spec_interactions_test.cljc index 7c90b625c..a84019496 100644 --- a/common/test/app/common/spec_interactions_test.cljc +++ b/common/test/app/common/types/shape/spec_interactions_test.cljc @@ -4,20 +4,20 @@ ;; ;; Copyright (c) UXBOX Labs SL -(ns app.common.spec-interactions-test +(ns app.common.types.shape.spec-interactions-test (:require [clojure.test :as t] [clojure.pprint :refer [pprint]] [app.common.exceptions :as ex] - [app.common.pages.init :as cpi] + [app.common.types.shape :as cts] [app.common.types.shape.interactions :as ctsi] [app.common.uuid :as uuid] [app.common.geom.point :as gpt])) (t/deftest set-event-type (let [interaction ctsi/default-interaction - shape (cpi/make-minimal-shape :rect) - frame (cpi/make-minimal-shape :frame)] + shape (cts/make-minimal-shape :rect) + frame (cts/make-minimal-shape :frame)] (t/testing "Set event type unchanged" (let [new-interaction @@ -148,7 +148,7 @@ (t/deftest option-delay - (let [frame (cpi/make-minimal-shape :frame) + (let [frame (cts/make-minimal-shape :frame) i1 ctsi/default-interaction i2 (ctsi/set-event-type i1 :after-delay frame)] @@ -211,10 +211,10 @@ (t/deftest option-overlay-opts - (let [base-frame (-> (cpi/make-minimal-shape :frame) + (let [base-frame (-> (cts/make-minimal-shape :frame) (assoc-in [:selrect :width] 100) (assoc-in [:selrect :height] 100)) - overlay-frame (-> (cpi/make-minimal-shape :frame) + overlay-frame (-> (cts/make-minimal-shape :frame) (assoc-in [:selrect :width] 30) (assoc-in [:selrect :height] 20)) objects {(:id base-frame) base-frame @@ -542,12 +542,12 @@ (t/deftest remap-interactions - (let [frame1 (cpi/make-minimal-shape :frame) - frame2 (cpi/make-minimal-shape :frame) - frame3 (cpi/make-minimal-shape :frame) - frame4 (cpi/make-minimal-shape :frame) - frame5 (cpi/make-minimal-shape :frame) - frame6 (cpi/make-minimal-shape :frame) + (let [frame1 (cts/make-minimal-shape :frame) + frame2 (cts/make-minimal-shape :frame) + frame3 (cts/make-minimal-shape :frame) + frame4 (cts/make-minimal-shape :frame) + frame5 (cts/make-minimal-shape :frame) + frame6 (cts/make-minimal-shape :frame) objects {(:id frame3) frame3 (:id frame4) frame4 diff --git a/frontend/resources/images/icons/component-copy.svg b/frontend/resources/images/icons/component-copy.svg new file mode 100644 index 000000000..ab45fb4a8 --- /dev/null +++ b/frontend/resources/images/icons/component-copy.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/resources/styles/main/partials/modal.scss b/frontend/resources/styles/main/partials/modal.scss index 4b8c59e2b..009ef72b9 100644 --- a/frontend/resources/styles/main/partials/modal.scss +++ b/frontend/resources/styles/main/partials/modal.scss @@ -134,6 +134,12 @@ font-size: $fs16; font-weight: 400; } + &.delete-shared { + padding: 15px 32px; + .modal-item-element { + font-size: $fs16; + } + } } .modal-footer { @@ -179,7 +185,8 @@ } } -.confirm-dialog { +.confirm-dialog, +.alert-dialog { background-color: $color-white; p { diff --git a/frontend/src/app/config.cljs b/frontend/src/app/config.cljs index 1de776e7c..a9e99252b 100644 --- a/frontend/src/app/config.cljs +++ b/frontend/src/app/config.cljs @@ -63,6 +63,11 @@ flags (sequence (map keyword) (str/words flags))] (flags/parse flags/default default-flags flags))) +(defn- parse-features + [global] + (when-let [features-str (obj/get global "penpotFeatures")] + (map keyword (str/words features-str)))) + (defn- parse-version [global] (-> (obj/get global "penpotVersion") @@ -88,6 +93,7 @@ (def build-date (parse-build-date global)) (def flags (atom (parse-flags global))) +(def features (atom (parse-features global))) (def version (atom (parse-version global))) (def target (atom (parse-target global))) (def browser (atom (parse-browser))) diff --git a/frontend/src/app/main.cljs b/frontend/src/app/main.cljs index a7e38e185..a72475f79 100644 --- a/frontend/src/app/main.cljs +++ b/frontend/src/app/main.cljs @@ -16,7 +16,9 @@ [app.main.sentry :as sentry] [app.main.store :as st] [app.main.ui :as ui] + [app.main.ui.alert] [app.main.ui.confirm] + [app.main.ui.delete-shared] [app.main.ui.modal :refer [modal]] [app.main.ui.routes :as rt] [app.main.worker :as worker] diff --git a/frontend/src/app/main/data/dashboard.cljs b/frontend/src/app/main/data/dashboard.cljs index 93efdb0ba..24f629343 100644 --- a/frontend/src/app/main/data/dashboard.cljs +++ b/frontend/src/app/main/data/dashboard.cljs @@ -13,6 +13,7 @@ [app.main.data.fonts :as df] [app.main.data.media :as di] [app.main.data.users :as du] + [app.main.features :as features] [app.main.repo :as rp] [app.util.i18n :as i18n :refer [tr]] [app.util.router :as rt] @@ -246,6 +247,32 @@ (->> (rp/query :team-shared-files {:team-id team-id}) (rx/map shared-files-fetched)))))) +;; --- EVENT: Get files that use this shared-file + +(defn clean-temp-shared + [] + (ptk/reify ::clean-temp-shared + ptk/UpdateEvent + (update [_ state] + (assoc-in state [:dashboard-local :files-with-shared] nil)))) + +(defn library-using-files-fetched + [files] + (ptk/reify ::library-using-files-fetched + ptk/UpdateEvent + (update [_ state] + (let [files (d/index-by :id files)] + (assoc-in state [:dashboard-local :files-with-shared] files))))) + +(defn fetch-library-using-files + [file] + (ptk/reify ::fetch-library-using-files + ptk/WatchEvent + (watch [_ _ _] + (let [file-id (:id file)] + (->> (rp/query :library-using-files {:file-id file-id}) + (rx/map library-using-files-fetched)))))) + ;; --- EVENT: recent-files (defn recent-files-fetched @@ -718,12 +745,13 @@ (-deref [_] {:project-id project-id}) ptk/WatchEvent - (watch [it _ _] + (watch [it state _] (let [{:keys [on-success on-error] :or {on-success identity on-error rx/throw}} (meta params) name (name (gensym (str (tr "dashboard.new-file-prefix") " "))) - params (assoc params :name name)] + components-v2 (features/active-feature? state :components-v2) + params (assoc params :name name :components-v2 components-v2)] (->> (rp/mutation! :create-file params) (rx/tap on-success) diff --git a/frontend/src/app/main/data/viewer.cljs b/frontend/src/app/main/data/viewer.cljs index 54c042222..bd3a77457 100644 --- a/frontend/src/app/main/data/viewer.cljs +++ b/frontend/src/app/main/data/viewer.cljs @@ -10,9 +10,11 @@ [app.common.geom.point :as gpt] [app.common.pages.helpers :as cph] [app.common.spec :as us] + [app.common.types.shape-tree :as ctt] [app.common.types.shape.interactions :as ctsi] [app.main.data.comments :as dcm] [app.main.data.fonts :as df] + [app.main.features :as features] [app.main.repo :as rp] [app.util.globals :as ug] [app.util.router :as rt] @@ -99,9 +101,15 @@ (us/assert ::fetch-bundle-params params) (ptk/reify ::fetch-file ptk/WatchEvent - (watch [_ _ _] - (let [params' (cond-> {:file-id file-id} - (uuid? share-id) (assoc :share-id share-id))] + (watch [_ state _] + (let [components-v2 (features/active-feature? state :components-v2) + params' (cond-> {:file-id file-id} + (uuid? share-id) + (assoc :share-id share-id) + + :always + (assoc :components-v2 components-v2))] + (->> (rp/query :view-only-bundle params') (rx/mapcat (fn [{:keys [fonts] :as bundle}] @@ -116,8 +124,8 @@ (map (fn [page-id] (let [data (get-in file [:data :pages-index page-id])] [page-id (assoc data - :frames (cph/get-viewer-frames (:objects data)) - :all-frames (cph/get-viewer-frames (:objects data) {:all-frames? true}))]))) + :frames (ctt/get-viewer-frames (:objects data)) + :all-frames (ctt/get-viewer-frames (:objects data) {:all-frames? true}))]))) (into {}))] (ptk/reify ::bundle-fetched diff --git a/frontend/src/app/main/data/workspace.cljs b/frontend/src/app/main/data/workspace.cljs index 62dc383ad..0b6db38e1 100644 --- a/frontend/src/app/main/data/workspace.cljs +++ b/frontend/src/app/main/data/workspace.cljs @@ -14,13 +14,13 @@ [app.common.geom.proportions :as gpr] [app.common.geom.shapes :as gsh] [app.common.math :as mth] - [app.common.pages :as cp] [app.common.pages.changes-builder :as pcb] [app.common.pages.helpers :as cph] [app.common.spec :as us] [app.common.text :as txt] [app.common.transit :as t] [app.common.types.shape :as cts] + [app.common.types.shape-tree :as ctst] [app.common.uuid :as uuid] [app.config :as cfg] [app.main.data.events :as ev] @@ -59,7 +59,6 @@ [app.util.globals :as ug] [app.util.http :as http] [app.util.i18n :as i18n] - [app.util.names :as un] [app.util.router :as rt] [app.util.timers :as tm] [app.util.webapi :as wapi] @@ -157,7 +156,7 @@ :workspace-project project :workspace-file (assoc file :initialized true) :workspace-data (-> (:data file) - (cph/start-object-indices) + (ctst/start-object-indices) ;; DEBUG: Uncomment this to try out migrations in local without changing ;; the version number #_(assoc :version 17) @@ -215,7 +214,8 @@ (watch [_ state _] (if (contains? (get-in state [:workspace-data :pages-index]) page-id) (rx/of (dwp/preload-data-uris) - (dwth/watch-state-changes)) + (dwth/watch-state-changes) + (dwl/watch-component-changes)) (let [default-page-id (get-in state [:workspace-data :pages 0])] (rx/of (go-to-page default-page-id))))) @@ -270,8 +270,8 @@ ptk/WatchEvent (watch [it state _] (let [pages (get-in state [:workspace-data :pages-index]) - unames (un/retrieve-used-names pages) - name (un/generate-unique-name unames "Page-1") + unames (ctst/retrieve-used-names pages) + name (ctst/generate-unique-name unames "Page-1") changes (-> (pcb/empty-changes it) (pcb/add-empty-page id name))] @@ -285,9 +285,9 @@ (watch [it state _] (let [id (uuid/next) pages (get-in state [:workspace-data :pages-index]) - unames (un/retrieve-used-names pages) + unames (ctst/retrieve-used-names pages) page (get-in state [:workspace-data :pages-index page-id]) - name (un/generate-unique-name unames (:name page)) + name (ctst/generate-unique-name unames (:name page)) no_thumbnails_objects (->> (:objects page) (d/mapm (fn [_ val] (dissoc val :use-for-thumbnail?)))) @@ -991,7 +991,7 @@ (let [selected (wsh/lookup-selected state) pages (-> state :workspace-data :pages-index vals) get-frames (fn [{:keys [objects id] :as page}] - (->> (cph/get-frames objects) + (->> (ctst/get-frames objects) (sequence (comp (filter :use-for-thumbnail?) (map :id) @@ -1223,7 +1223,7 @@ ;; selected and its parents objects (cph/selected-subtree objects selected) - selected (->> (cph/sort-z-index objects selected) + selected (->> (ctst/sort-z-index objects selected) (into (d/ordered-set)))] (assoc data :selected selected))) @@ -1478,7 +1478,7 @@ [frame-id frame-id delta]) (empty? page-selected) - (let [frame-id (cph/frame-id-by-position page-objects mouse-pos) + (let [frame-id (ctst/frame-id-by-position page-objects mouse-pos) delta (gpt/subtract mouse-pos orig-pos)] [frame-id frame-id delta]) @@ -1590,8 +1590,8 @@ height 16 page-id (:current-page-id state) frame-id (-> (wsh/lookup-page-objects state page-id) - (cph/frame-id-by-position @ms/mouse-position)) - shape (cp/setup-rect-selrect + (ctst/frame-id-by-position @ms/mouse-position)) + shape (cts/setup-rect-selrect {:id id :type :text :name "Text" @@ -1681,12 +1681,12 @@ (let [srect (gsh/selection-rect selected-objs) frame-id (get-in objects [(first selected) :frame-id]) parent-id (get-in objects [(first selected) :parent-id]) - shape (-> (cp/make-minimal-shape :frame) + shape (-> (cts/make-minimal-shape :frame) (merge {:x (:x srect) :y (:y srect) :width (:width srect) :height (:height srect)}) (assoc :frame-id frame-id :parent-id parent-id) (cond-> (not= frame-id uuid/zero) (assoc :fills [] :hide-in-viewer true)) - (cp/setup-rect-selrect))] + (cts/setup-rect-selrect))] (rx/of (dwu/start-undo-transaction) (dwsh/add-shape shape) diff --git a/frontend/src/app/main/data/workspace/bool.cljs b/frontend/src/app/main/data/workspace/bool.cljs index 771c06bb2..70fe8d222 100644 --- a/frontend/src/app/main/data/workspace/bool.cljs +++ b/frontend/src/app/main/data/workspace/bool.cljs @@ -11,11 +11,11 @@ [app.common.pages.changes-builder :as pcb] [app.common.pages.helpers :as cph] [app.common.path.shapes-to-path :as stp] + [app.common.types.shape-tree :as ctt] [app.common.uuid :as uuid] [app.main.data.workspace.changes :as dch] [app.main.data.workspace.selection :as dws] [app.main.data.workspace.state-helpers :as wsh] - [app.util.names :as un] [beicon.core :as rx] [cuerdas.core :as str] [potok.core :as ptk])) @@ -90,8 +90,8 @@ (let [page-id (:current-page-id state) objects (wsh/lookup-page-objects state) base-name (-> bool-type d/name str/capital (str "-1")) - name (-> (un/retrieve-used-names objects) - (un/generate-unique-name base-name)) + name (-> (ctt/retrieve-used-names objects) + (ctt/generate-unique-name base-name)) shapes (selected-shapes state)] (when-not (empty? shapes) diff --git a/frontend/src/app/main/data/workspace/changes.cljs b/frontend/src/app/main/data/workspace/changes.cljs index 209c8f549..488c5ec2b 100644 --- a/frontend/src/app/main/data/workspace/changes.cljs +++ b/frontend/src/app/main/data/workspace/changes.cljs @@ -13,6 +13,7 @@ [app.common.pages.changes-spec :as pcs] [app.common.pages.helpers :as cph] [app.common.spec :as us] + [app.common.types.shape-tree :as ctst] [app.common.uuid :as uuid] [app.main.data.workspace.state-helpers :as wsh] [app.main.data.workspace.undo :as dwu] @@ -165,7 +166,7 @@ (update-in state path (fn [file] (-> file (cp/process-changes redo-changes false) - (cph/update-object-indices page-id)))) + (ctst/update-object-indices page-id)))) (catch :default err (log/error :js/error err) @@ -191,6 +192,7 @@ process-page-changes (fn [[page-id _changes]] (update-indices page-id redo-changes))] + (rx/concat (rx/from (map process-page-changes changes-by-pages)) diff --git a/frontend/src/app/main/data/workspace/common.cljs b/frontend/src/app/main/data/workspace/common.cljs index d9671bfed..6f9ac6d35 100644 --- a/frontend/src/app/main/data/workspace/common.cljs +++ b/frontend/src/app/main/data/workspace/common.cljs @@ -108,4 +108,3 @@ :undo-changes [] :origin it :save-undo? false}))))))))))) - diff --git a/frontend/src/app/main/data/workspace/drawing.cljs b/frontend/src/app/main/data/workspace/drawing.cljs index cb75f95cf..06384d41c 100644 --- a/frontend/src/app/main/data/workspace/drawing.cljs +++ b/frontend/src/app/main/data/workspace/drawing.cljs @@ -7,7 +7,7 @@ (ns app.main.data.workspace.drawing "Drawing interactions." (:require - [app.common.pages :as cp] + [app.common.types.shape :as cts] [app.common.uuid :as uuid] [app.main.data.workspace.common :as dwc] [app.main.data.workspace.drawing.box :as box] @@ -91,7 +91,7 @@ (ptk/reify ::handle-drawing ptk/UpdateEvent (update [_ state] - (let [data (cp/make-minimal-shape type)] + (let [data (cts/make-minimal-shape type)] (update-in state [:workspace-drawing :object] merge data))) ptk/WatchEvent diff --git a/frontend/src/app/main/data/workspace/drawing/box.cljs b/frontend/src/app/main/data/workspace/drawing/box.cljs index bdffbe1f7..1868eb61b 100644 --- a/frontend/src/app/main/data/workspace/drawing/box.cljs +++ b/frontend/src/app/main/data/workspace/drawing/box.cljs @@ -9,8 +9,9 @@ [app.common.geom.point :as gpt] [app.common.geom.shapes :as gsh] [app.common.math :as mth] - [app.common.pages :as cp] [app.common.pages.helpers :as cph] + [app.common.types.shape :as cts] + [app.common.types.shape-tree :as ctt] [app.common.uuid :as uuid] [app.main.data.workspace.drawing.common :as common] [app.main.data.workspace.state-helpers :as wsh] @@ -65,11 +66,11 @@ focus (:workspace-focus-selected state) zoom (get-in state [:workspace-local :zoom] 1) - fid (cph/frame-id-by-position objects initial) + fid (ctt/frame-id-by-position objects initial) shape (get-in state [:workspace-drawing :object]) shape (-> shape - (cp/setup-shape {:x (:x initial) + (cts/setup-shape {:x (:x initial) :y (:y initial) :width 0.01 :height 0.01}) diff --git a/frontend/src/app/main/data/workspace/drawing/common.cljs b/frontend/src/app/main/data/workspace/drawing/common.cljs index 03f956aa5..98543f82b 100644 --- a/frontend/src/app/main/data/workspace/drawing/common.cljs +++ b/frontend/src/app/main/data/workspace/drawing/common.cljs @@ -9,8 +9,8 @@ [app.common.geom.matrix :as gmt] [app.common.geom.shapes :as gsh] [app.common.math :as mth] - [app.common.pages :as cp] [app.common.pages.helpers :as cph] + [app.common.types.shape :as cts] [app.main.data.workspace.shapes :as dwsh] [app.main.data.workspace.state-helpers :as wsh] [app.main.data.workspace.undo :as dwu] @@ -55,7 +55,7 @@ (assoc :height 17 :width 4 :grow-type :auto-width) click-draw? - (cp/setup-rect-selrect) + (cts/setup-rect-selrect) :always (-> (gsh/transform-shape) diff --git a/frontend/src/app/main/data/workspace/drawing/curve.cljs b/frontend/src/app/main/data/workspace/drawing/curve.cljs index d60cc040c..66062fd16 100644 --- a/frontend/src/app/main/data/workspace/drawing/curve.cljs +++ b/frontend/src/app/main/data/workspace/drawing/curve.cljs @@ -8,7 +8,7 @@ (:require [app.common.geom.shapes :as gsh] [app.common.geom.shapes.path :as gsp] - [app.common.pages.helpers :as cph] + [app.common.types.shape-tree :as ctt] [app.main.data.workspace.drawing.common :as common] [app.main.data.workspace.state-helpers :as wsh] [app.main.streams :as ms] @@ -47,7 +47,7 @@ (let [objects (wsh/lookup-page-objects state) content (get-in state [:workspace-drawing :object :content] []) position (get-in content [0 :params] nil) - frame-id (cph/frame-id-by-position objects position)] + frame-id (ctt/frame-id-by-position objects position)] (-> state (assoc-in [:workspace-drawing :object :frame-id] frame-id)))))) diff --git a/frontend/src/app/main/data/workspace/groups.cljs b/frontend/src/app/main/data/workspace/groups.cljs index 2b315fc5e..5906db729 100644 --- a/frontend/src/app/main/data/workspace/groups.cljs +++ b/frontend/src/app/main/data/workspace/groups.cljs @@ -8,13 +8,13 @@ (:require [app.common.data :as d] [app.common.geom.shapes :as gsh] - [app.common.pages :as cp] [app.common.pages.changes-builder :as pcb] [app.common.pages.helpers :as cph] + [app.common.types.shape :as cts] + [app.common.types.shape-tree :as ctt] [app.main.data.workspace.changes :as dch] [app.main.data.workspace.selection :as dws] [app.main.data.workspace.state-helpers :as wsh] - [app.util.names :as un] [beicon.core :as rx] [potok.core :as ptk])) @@ -71,12 +71,12 @@ (= (count shapes) 1) (= (:type (first shapes)) :group)) (:name (first shapes)) - (-> (un/retrieve-used-names objects) - (un/generate-unique-name base-name))) + (-> (ctt/retrieve-used-names objects) + (ctt/generate-unique-name base-name))) selrect (gsh/selection-rect shapes) - group (-> (cp/make-minimal-group frame-id selrect gname) - (cp/setup-shape selrect) + group (-> (cts/make-minimal-group frame-id selrect gname) + (cts/setup-shape selrect) (assoc :shapes (mapv :id shapes) :parent-id parent-id :frame-id frame-id diff --git a/frontend/src/app/main/data/workspace/interactions.cljs b/frontend/src/app/main/data/workspace/interactions.cljs index 40c6a80f9..e1a653be7 100644 --- a/frontend/src/app/main/data/workspace/interactions.cljs +++ b/frontend/src/app/main/data/workspace/interactions.cljs @@ -12,12 +12,12 @@ [app.common.pages.helpers :as cph] [app.common.spec :as us] [app.common.types.page :as ctp] + [app.common.types.shape-tree :as ctst] [app.common.types.shape.interactions :as ctsi] [app.common.uuid :as uuid] [app.main.data.workspace.changes :as dch] [app.main.data.workspace.state-helpers :as wsh] [app.main.streams :as ms] - [app.util.names :as un] [beicon.core :as rx] [potok.core :as ptk])) @@ -32,7 +32,7 @@ flows (get-in page [:options :flows] []) unames (into #{} (map :name flows)) - name (un/generate-unique-name unames "Flow-1") + name (ctst/generate-unique-name unames "Flow-1") new-flow {:id (uuid/next) :name name @@ -182,7 +182,7 @@ from-frame-id (if (cph/frame-shape? from-shape) from-id (:frame-id from-shape)) - target-frame (cph/frame-by-position objects position)] + target-frame (ctst/frame-by-position objects position)] (when (and (not= (:id target-frame) uuid/zero) (not= (:id target-frame) from-frame-id) diff --git a/frontend/src/app/main/data/workspace/libraries.cljs b/frontend/src/app/main/data/workspace/libraries.cljs index 7663a0585..d460cbf89 100644 --- a/frontend/src/app/main/data/workspace/libraries.cljs +++ b/frontend/src/app/main/data/workspace/libraries.cljs @@ -10,12 +10,15 @@ [app.common.geom.point :as gpt] [app.common.logging :as log] [app.common.pages :as cp] + [app.common.pages.changes :as ch] [app.common.pages.changes-builder :as pcb] [app.common.pages.changes-spec :as pcs] [app.common.pages.helpers :as cph] [app.common.spec :as us] [app.common.types.color :as ctc] + [app.common.types.container :as ctn] [app.common.types.file :as ctf] + [app.common.types.shape-tree :as ctst] [app.common.types.typography :as ctt] [app.common.uuid :as uuid] [app.main.data.dashboard :as dd] @@ -27,10 +30,11 @@ [app.main.data.workspace.selection :as dws] [app.main.data.workspace.state-helpers :as wsh] [app.main.data.workspace.undo :as dwu] + [app.main.features :as features] + [app.main.refs :as refs] [app.main.repo :as rp] [app.main.store :as st] [app.util.i18n :refer [tr]] - [app.util.names :as un] [app.util.router :as rt] [app.util.time :as dt] [beicon.core :as rx] @@ -137,7 +141,7 @@ (pcb/update-color color))] (rx/of (dwu/start-undo-transaction) (dch/commit-changes changes) - (sync-file (:current-file-id state) file-id) + (sync-file (:current-file-id state) file-id :colors (:id color)) (dwu/commit-undo-transaction)))) (defn update-color @@ -240,7 +244,7 @@ (pcb/update-typography typography))] (rx/of (dwu/start-undo-transaction) (dch/commit-changes changes) - (sync-file (:current-file-id state) file-id) + (sync-file (:current-file-id state) file-id :typographies (:id typography)) (dwu/commit-undo-transaction)))) (defn update-typography @@ -280,7 +284,7 @@ (defn- add-component2 "This is the second step of the component creation." - [selected] + [selected components-v2] (ptk/reify ::add-component2 IDeref (-deref [_] {:num-shapes (count selected)}) @@ -293,7 +297,7 @@ shapes (dwg/shapes-for-grouping objects selected)] (when-not (empty? shapes) (let [[group _ changes] - (dwlh/generate-add-component it shapes objects page-id file-id)] + (dwlh/generate-add-component it shapes objects page-id file-id components-v2)] (when-not (empty? (:redo-changes changes)) (rx/of (dch/commit-changes changes) (dws/select-shapes (d/ordered-set (:id group))))))))))) @@ -307,10 +311,11 @@ (ptk/reify ::add-component ptk/WatchEvent (watch [_ state _] - (let [objects (wsh/lookup-page-objects state) - selected (->> (wsh/lookup-selected state) - (cph/clean-loops objects))] - (rx/of (add-component2 selected)))))) + (let [objects (wsh/lookup-page-objects state) + selected (->> (wsh/lookup-selected state) + (cph/clean-loops objects)) + components-v2 (features/active-feature? state :components-v2)] + (rx/of (add-component2 selected components-v2)))))) (defn rename-component "Rename the component with the given id, in the current file library." @@ -352,18 +357,30 @@ component (cph/get-component libraries id) all-components (-> state :workspace-data :components vals) unames (into #{} (map :name) all-components) - new-name (un/generate-unique-name unames (:name component)) + new-name (ctst/generate-unique-name unames (:name component)) - [new-shape new-shapes _updated-shapes] - (dwlh/duplicate-component component) + components-v2 (features/active-feature? state :components-v2) - changes (-> (pcb/empty-changes it nil) ;; no objects are changed - (pcb/with-objects nil) ;; in the current page - (pcb/add-component (:id new-shape) + main-instance-page (when components-v2 + (wsh/lookup-page state (:main-instance-page component))) + main-instance-shape (when components-v2 + (ctn/get-shape main-instance-page (:main-instance-id component))) + + [new-component-shape new-component-shapes + new-main-instance-shape new-main-instance-shapes] + (dwlh/duplicate-component component main-instance-page main-instance-shape) + + changes (-> (pcb/empty-changes it nil) + (pcb/with-page main-instance-page) + (pcb/with-objects (:objects main-instance-page)) + (pcb/add-objects new-main-instance-shapes {:ignore-touched true}) + (pcb/add-component (:id new-component-shape) (:path component) new-name - new-shapes - []))] + new-component-shapes + [] + (:id new-main-instance-shape) + (:id main-instance-page)))] (rx/of (dch/commit-changes changes)))))) @@ -521,7 +538,7 @@ libraries (wsh/get-libraries state) container (cph/get-container local-file :page page-id) - shape (cph/get-shape container id) + shape (ctn/get-shape container id) changes (-> (pcb/empty-changes it) @@ -568,13 +585,15 @@ (ptk/reify ::update-component-sync ptk/WatchEvent (watch [_ state _] - (let [current-file-id (:current-file-id state)] + (let [current-file-id (:current-file-id state) + page (wsh/lookup-page state) + shape (ctn/get-shape page shape-id)] (rx/of (dwu/start-undo-transaction) (update-component shape-id) - (sync-file current-file-id file-id) + (sync-file current-file-id file-id :components (:component-id shape)) (when (not= current-file-id file-id) - (sync-file file-id file-id)) + (sync-file file-id file-id :components (:component-id shape))) (dwu/commit-undo-transaction)))))) (defn update-component-in-bulk @@ -593,63 +612,83 @@ "Synchronize the given file from the given library. Walk through all shapes in all pages in the file that use some color, typography or component of the library, and copy the new values to the shapes. Do - it also for shapes inside components of the local file library." - [file-id library-id] - (us/assert ::us/uuid file-id) - (us/assert ::us/uuid library-id) - (ptk/reify ::sync-file - ptk/UpdateEvent - (update [_ state] - (if (not= library-id (:current-file-id state)) - (d/assoc-in-when state [:workspace-libraries library-id :synced-at] (dt/now)) - state)) + it also for shapes inside components of the local file library. - ptk/WatchEvent - (watch [it state _] - (when (and (some? file-id) (some? library-id)) ; Prevent race conditions while navigating out of the file - (log/info :msg "SYNC-FILE" - :file (dwlh/pretty-file file-id state) - :library (dwlh/pretty-file library-id state)) - (let [file (wsh/get-file state file-id) + If it's known that only one asset has changed, you can give its + type and id, and only shapes that use it will be synced, thus avoiding + a lot of unneeded checks." + ([file-id library-id] + (sync-file file-id library-id nil nil)) + ([file-id library-id asset-type asset-id] + (us/assert ::us/uuid file-id) + (us/assert ::us/uuid library-id) + (us/assert (s/nilable #{:colors :components :typographies}) asset-type) + (us/assert (s/nilable ::us/uuid) asset-id) + (ptk/reify ::sync-file + ptk/UpdateEvent + (update [_ state] + (if (and (not= library-id (:current-file-id state)) + (nil? asset-id)) + (d/assoc-in-when state [:workspace-libraries library-id :synced-at] (dt/now)) + state)) - library-changes (reduce - pcb/concat-changes - (pcb/empty-changes it) - [(dwlh/generate-sync-library it file-id :components library-id state) - (dwlh/generate-sync-library it file-id :colors library-id state) - (dwlh/generate-sync-library it file-id :typographies library-id state)]) - file-changes (reduce - pcb/concat-changes - (pcb/empty-changes it) - [(dwlh/generate-sync-file it file-id :components library-id state) - (dwlh/generate-sync-file it file-id :colors library-id state) - (dwlh/generate-sync-file it file-id :typographies library-id state)]) + ptk/WatchEvent + (watch [it state _] + (when (and (some? file-id) (some? library-id)) ; Prevent race conditions while navigating out of the file + (log/info :msg "SYNC-FILE" + :file (dwlh/pretty-file file-id state) + :library (dwlh/pretty-file library-id state)) + (let [file (wsh/get-file state file-id) - changes (pcb/concat-changes library-changes file-changes)] + sync-components? (or (nil? asset-type) (= asset-type :components)) + sync-colors? (or (nil? asset-type) (= asset-type :colors)) + sync-typographies? (or (nil? asset-type) (= asset-type :typographies)) - (log/debug :msg "SYNC-FILE finished" :js/rchanges (log-changes - (:redo-changes changes) - file)) - (rx/concat - (rx/of (dm/hide-tag :sync-dialog)) - (when (seq (:redo-changes changes)) - (rx/of (dch/commit-changes (assoc changes ;; TODO a ver qué pasa con esto - :file-id file-id)))) - (when (not= file-id library-id) - ;; When we have just updated the library file, give some time for the - ;; update to finish, before marking this file as synced. - ;; TODO: look for a more precise way of syncing this. - ;; Maybe by using the stream (second argument passed to watch) - ;; to wait for the corresponding changes-committed and then proceed - ;; with the :update-sync mutation. - (rx/concat (rx/timer 3000) - (rp/mutation :update-sync - {:file-id file-id - :library-id library-id}))) - (when (seq (:redo-changes library-changes)) - (rx/of (sync-file-2nd-stage file-id library-id))))))))) + library-changes (reduce + pcb/concat-changes + (pcb/empty-changes it) + [(when sync-components? + (dwlh/generate-sync-library it file-id :components asset-id library-id state)) + (when sync-colors? + (dwlh/generate-sync-library it file-id :colors asset-id library-id state)) + (when sync-typographies? + (dwlh/generate-sync-library it file-id :typographies asset-id library-id state))]) + file-changes (reduce + pcb/concat-changes + (pcb/empty-changes it) + [(when sync-components? + (dwlh/generate-sync-file it file-id :components asset-id library-id state)) + (when sync-colors? + (dwlh/generate-sync-file it file-id :colors asset-id library-id state)) + (when sync-typographies? + (dwlh/generate-sync-file it file-id :typographies asset-id library-id state))]) -(defn sync-file-2nd-stage + changes (pcb/concat-changes library-changes file-changes)] + + (log/debug :msg "SYNC-FILE finished" :js/rchanges (log-changes + (:redo-changes changes) + file)) + (rx/concat + (rx/of (dm/hide-tag :sync-dialog)) + (when (seq (:redo-changes changes)) + (rx/of (dch/commit-changes (assoc changes ;; TODO a ver qué pasa con esto + :file-id file-id)))) + (when (not= file-id library-id) + ;; When we have just updated the library file, give some time for the + ;; update to finish, before marking this file as synced. + ;; TODO: look for a more precise way of syncing this. + ;; Maybe by using the stream (second argument passed to watch) + ;; to wait for the corresponding changes-committed and then proceed + ;; with the :update-sync mutation. + (rx/concat (rx/timer 3000) + (rp/mutation :update-sync + {:file-id file-id + :library-id library-id}))) + (when (and (seq (:redo-changes library-changes)) + sync-components?) + (rx/of (sync-file-2nd-stage file-id library-id asset-id)))))))))) + +(defn- sync-file-2nd-stage "If some components have been modified, we need to launch another synchronization to update the instances of the changed components." ;; TODO: this does not work if there are multiple nested components. Only the @@ -658,9 +697,10 @@ ;; recursively. But for this not to cause an infinite loop, we need to ;; implement updated-at at component level, to detect what components have ;; not changed, and then not to apply sync and terminate the loop. - [file-id library-id] + [file-id library-id asset-id] (us/assert ::us/uuid file-id) (us/assert ::us/uuid library-id) + (us/assert (s/nilable ::us/uuid) asset-id) (ptk/reify ::sync-file-2nd-stage ptk/WatchEvent (watch [it state _] @@ -671,8 +711,8 @@ changes (reduce pcb/concat-changes (pcb/empty-changes it) - [(dwlh/generate-sync-file it file-id :components library-id state) - (dwlh/generate-sync-library it file-id :components library-id state)])] + [(dwlh/generate-sync-file it file-id :components asset-id library-id state) + (dwlh/generate-sync-library it file-id :components asset-id library-id state)])] (when (seq (:redo-changes changes)) (log/debug :msg "SYNC-FILE (2nd stage) finished" :js/rchanges (log-changes (:redo-changes changes) @@ -716,6 +756,48 @@ :callback do-dismiss}] :sync-dialog)))))) +(defn watch-component-changes + "Watch the state for changes that affect to any main instance. If a change is detected will throw + an update-component-sync, so changes are immediately propagated to the component and copies." + [] + (ptk/reify ::watch-component-changes + ptk/WatchEvent + (watch [_ state stream] + (let [components-v2 (features/active-feature? state :components-v2) + + stopper + (->> stream + (rx/filter #(or (= :app.main.data.workspace/finalize-page (ptk/type %)) + (= ::watch-component-changes (ptk/type %))))) + + workspace-data-s + (->> (rx/concat + (rx/of nil) + (rx/from-atom refs/workspace-data {:emit-current-value? true}))) + + change-s + (->> stream + (rx/filter #(or (dch/commit-changes? %) + (= (ptk/type %) :app.main.data.workspace.notifications/handle-file-change))) + (rx/observe-on :async)) + + check-changes + (fn [[event data]] + (let [changes (-> event deref :changes) + components-changed (reduce #(into %1 (ch/components-changed data %2)) + #{} + changes)] + (when (d/not-empty? components-changed) + (run! st/emit! + (map #(update-component-sync % (:id data)) + components-changed)))))] + + (when components-v2 + (->> change-s + (rx/with-latest-from workspace-data-s) + (rx/map check-changes) + (rx/take-until stopper))))))) + ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Backend interactions ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; @@ -762,12 +844,13 @@ [file-id library-id] (ptk/reify ::attach-library ptk/WatchEvent - (watch [_ _ _] - (let [fetched #(assoc-in %2 [:workspace-libraries (:id %1)] %1) - params {:file-id file-id - :library-id library-id}] + (watch [_ state _] + (let [components-v2 (features/active-feature? state :components-v2) + fetched #(assoc-in %2 [:workspace-libraries (:id %1)] %1) + params {:file-id file-id + :library-id library-id}] (->> (rp/mutation :link-file-to-library params) - (rx/mapcat #(rp/query :file {:id library-id})) + (rx/mapcat #(rp/query :file {:id library-id :components-v2 components-v2})) (rx/map #(partial fetched %))))))) (defn unlink-file-from-library diff --git a/frontend/src/app/main/data/workspace/libraries_helpers.cljs b/frontend/src/app/main/data/workspace/libraries_helpers.cljs index a1e50c89f..195caf140 100644 --- a/frontend/src/app/main/data/workspace/libraries_helpers.cljs +++ b/frontend/src/app/main/data/workspace/libraries_helpers.cljs @@ -16,9 +16,12 @@ [app.common.spec :as us] [app.common.text :as txt] [app.common.types.color :as ctc] + [app.common.types.component :as ctk] + [app.common.types.container :as ctn] + [app.common.types.shape-tree :as ctst] + [app.common.types.typography :as cty] [app.main.data.workspace.groups :as dwg] [app.main.data.workspace.state-helpers :as wsh] - [app.util.names :as un] [cljs.spec.alpha :as s] [clojure.set :as set])) @@ -55,51 +58,11 @@ ;; ---- Components and instances creation ---- -(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 - to the new ones." - [shape objects file-id] - (assert (nil? (:component-id shape))) - (assert (nil? (:component-file shape))) - (assert (nil? (:shape-ref shape))) - (let [;; Ensure that the component root is not an instance and - ;; it's no longer tied to a frame. - update-new-shape (fn [new-shape _original-shape] - (cond-> new-shape - true - (-> (assoc :frame-id nil) - (dissoc :component-root?)) - - (nil? (:parent-id new-shape)) - (dissoc :component-id - :component-file - :shape-ref))) - - ;; Make the original shape an instance of the new component. - ;; If one of the original shape children already was a component - ;; instance, maintain this instanceness untouched. - update-original-shape (fn [original-shape new-shape] - (cond-> original-shape - (nil? (:shape-ref original-shape)) - (-> (assoc :shape-ref (:id new-shape)) - (dissoc :touched)) - - (nil? (:parent-id new-shape)) - (assoc :component-id (:id new-shape) - :component-file file-id - :component-root? true) - - (some? (:parent-id new-shape)) - (dissoc :component-root?)))] - - (cph/clone-object shape nil objects update-new-shape update-original-shape))) - (defn generate-add-component "If there is exactly one id, and it's a group, use it as root. Otherwise, create a group that contains all ids. Then, make a component with it, and link all shapes to their corresponding one in the component." - [it shapes objects page-id file-id] + [it shapes objects page-id file-id components-v2] (if (and (= (count shapes) 1) (:component-id (first shapes))) [(first shapes) (pcb/empty-changes it)] @@ -114,73 +77,54 @@ (dwg/prepare-create-group it objects page-id shapes name true)) [new-shape new-shapes updated-shapes] - (make-component-shape group objects file-id) + (ctn/make-component-shape group objects file-id components-v2) changes (-> changes (pcb/add-component (:id new-shape) path name new-shapes - updated-shapes))] + updated-shapes + (:id group) + page-id))] [group new-shape changes]))) (defn duplicate-component "Clone the root shape of the component and all children. Generate new ids from all of them." - [component] - (let [component-root (cph/get-component-root component)] - (cph/clone-object component-root - nil - (get component :objects) - identity))) + [component main-instance-page main-instance-shape] + (let [position (gpt/add (gpt/point (:x main-instance-shape) (:y main-instance-shape)) + (gpt/point (+ (:width main-instance-shape) 50) 0)) + + component-root (ctk/get-component-root component) + + [new-component-shape new-component-shapes _] + (ctst/clone-object component-root + nil + (get component :objects) + identity) + + + [new-instance-shape new-instance-shapes] + (when (and (some? main-instance-page) (some? main-instance-shape)) + (ctn/make-component-instance main-instance-page + {:id (:id new-component-shape) + :name (:name new-component-shape) + :objects (d/index-by :id new-component-shapes)} + (:component-file main-instance-shape) + position + false))] + + [new-component-shape new-component-shapes + new-instance-shape new-instance-shapes])) (defn generate-instantiate-component "Generate changes to create a new instance from a component." [it file-id component-id position page libraries] (let [component (cph/get-component libraries file-id component-id) - component-shape (cph/get-shape component component-id) - orig-pos (gpt/point (:x component-shape) (:y component-shape)) - delta (gpt/subtract position orig-pos) - - objects (:objects page) - unames (volatile! (un/retrieve-used-names objects)) - - frame-id (cph/frame-id-by-position objects (gpt/add orig-pos delta)) - - update-new-shape - (fn [new-shape original-shape] - (let [new-name (un/generate-unique-name @unames (:name new-shape))] - - (when (nil? (:parent-id original-shape)) - (vswap! unames conj new-name)) - - (cond-> new-shape - true - (as-> $ - (gsh/move $ delta) - (assoc $ :frame-id frame-id) - (assoc $ :parent-id - (or (:parent-id $) (:frame-id $))) - (dissoc $ :touched)) - - (nil? (:shape-ref original-shape)) - (assoc :shape-ref (:id original-shape)) - - (nil? (:parent-id original-shape)) - (assoc :component-id (:id original-shape) - :component-file file-id - :component-root? true - :name new-name) - - (some? (:parent-id original-shape)) - (dissoc :component-root?)))) - - [new-shape new-shapes _] - (cph/clone-object component-shape - nil - (get component :objects) - update-new-shape) + [new-shape new-shapes] + (ctn/make-component-instance page component file-id position false) changes (reduce #(pcb/add-object %1 %2 {:ignore-touched true}) (pcb/empty-changes it (:id page)) @@ -212,14 +156,19 @@ (defn generate-sync-file "Generate changes to synchronize all shapes in all pages of the given file, - that use assets of the given type in the given library." - [it file-id asset-type library-id state] + that use assets of the given type in the given library. + + If an asset id is given, only shapes linked to this particular asset will + be syncrhonized." + [it file-id asset-type asset-id library-id state] (s/assert #{:colors :components :typographies} asset-type) + (s/assert (s/nilable ::us/uuid) asset-id) (s/assert ::us/uuid file-id) (s/assert ::us/uuid library-id) (log/info :msg "Sync file with library" :asset-type asset-type + :asset-id asset-id :file (pretty-file file-id state) :library (pretty-file library-id state)) @@ -232,6 +181,7 @@ changes (generate-sync-container it asset-type + asset-id library-id state (cph/make-container page :page)))) @@ -240,11 +190,19 @@ (defn generate-sync-library "Generate changes to synchronize all shapes in all components of the local library of the given file, that use assets of the given type in - the given library." - [it file-id asset-type library-id state] + the given library. + + If an asset id is given, only shapes linked to this particular asset will + be syncrhonized." + [it file-id asset-type asset-id library-id state] + (s/assert #{:colors :components :typographies} asset-type) + (s/assert (s/nilable ::us/uuid) asset-id) + (s/assert ::us/uuid file-id) + (s/assert ::us/uuid library-id) (log/info :msg "Sync local components with library" :asset-type asset-type + :asset-id asset-id :file (pretty-file file-id state) :library (pretty-file library-id state)) @@ -257,6 +215,7 @@ changes (generate-sync-container it asset-type + asset-id library-id state (cph/make-container local-component :component)))) @@ -265,14 +224,14 @@ (defn- generate-sync-container "Generate changes to synchronize all shapes in a particular container (a page or a component) that use assets of the given type in the given library." - [it asset-type library-id state container] + [it asset-type asset-id library-id state container] (if (cph/page? container) (log/debug :msg "Sync page in local file" :page-id (:id container)) (log/debug :msg "Sync component in local library" :component-id (:id container))) - (let [linked-shapes (->> (vals (:objects container)) - (filter #(uses-assets? asset-type % library-id (cph/page? container))))] + (let [linked-shapes (->> (vals (:objects container)) + (filter #(uses-assets? asset-type asset-id % library-id (cph/page? container))))] (loop [shapes (seq linked-shapes) changes (-> (pcb/empty-changes it) (pcb/with-container container) @@ -289,27 +248,26 @@ (defmulti uses-assets? "Checks if a shape uses some asset of the given type in the given library." - (fn [asset-type _ _ _] asset-type)) + (fn [asset-type _ _ _ _] asset-type)) (defmethod uses-assets? :components - [_ shape library-id page?] - (and (some? (:component-id shape)) - (= (:component-file shape) library-id) + [_ component-id shape library-id page?] + (and (if (nil? component-id) + (ctk/uses-library-components? shape library-id) + (ctk/instance-of? shape library-id component-id)) (or (:component-root? shape) (not page?)))) ; avoid nested components inside pages (defmethod uses-assets? :colors - [_ shape library-id _] - (ctc/uses-library-colors? shape library-id)) + [_ color-id shape library-id _] + (if (nil? color-id) + (ctc/uses-library-colors? shape library-id) + (ctc/uses-library-color? shape library-id color-id))) (defmethod uses-assets? :typographies - [_ shape library-id _] - (and (= (:type shape) :text) - (->> shape - :content - ;; Check if any node in the content has a reference for the library - (txt/node-seq - #(and (some? (:typography-ref-id %)) - (= (:typography-ref-file %) library-id)))))) + [_ typography-id shape library-id _] + (if (nil? typography-id) + (cty/uses-library-typographies? shape library-id) + (cty/uses-library-typography? shape library-id typography-id))) (defmulti generate-sync-shape "Generate changes to synchronize one shape from all assets of the given type @@ -482,18 +440,18 @@ instance, and all its children, from the given component." [changes libraries container shape-id reset?] (log/debug :msg "Sync shape direct" :shape (str shape-id) :reset? reset?) - (let [shape-inst (cph/get-shape container shape-id) + (let [shape-inst (ctn/get-shape container shape-id) component (cph/get-component libraries (:component-file shape-inst) (:component-id shape-inst)) shape-main (when component - (cph/get-shape component (:shape-ref shape-inst))) + (ctn/get-shape component (:shape-ref shape-inst))) initial-root? (:component-root? shape-inst) root-inst shape-inst root-main (when component - (cph/get-component-root component))] + (ctk/get-component-root component))] (if component (generate-sync-shape-direct-recursive changes @@ -543,9 +501,9 @@ set-remote-synced? (change-remote-synced shape-inst container true)) - children-inst (mapv #(cph/get-shape container %) + children-inst (mapv #(ctn/get-shape container %) (:shapes shape-inst)) - children-main (mapv #(cph/get-shape component %) + children-main (mapv #(ctn/get-shape component %) (:shapes shape-main)) only-inst (fn [changes child-inst] @@ -608,16 +566,16 @@ the values in the shape and all its children." [changes libraries container shape-id] (log/debug :msg "Sync shape inverse" :shape (str shape-id)) - (let [shape-inst (cph/get-shape container shape-id) + (let [shape-inst (ctn/get-shape container shape-id) component (cph/get-component libraries (:component-file shape-inst) (:component-id shape-inst)) - shape-main (cph/get-shape component (:shape-ref shape-inst)) + shape-main (ctn/get-shape component (:shape-ref shape-inst)) initial-root? (:component-root? shape-inst) root-inst shape-inst - root-main (cph/get-component-root component)] + root-main (ctk/get-component-root component)] (if component (generate-sync-shape-inverse-recursive changes @@ -668,9 +626,9 @@ set-remote-synced? (change-remote-synced shape-inst container true)) - children-inst (mapv #(cph/get-shape container %) + children-inst (mapv #(ctn/get-shape container %) (:shapes shape-inst)) - children-main (mapv #(cph/get-shape component %) + children-main (mapv #(ctn/get-shape component %) (:shapes shape-main)) only-inst (fn [changes child-inst] @@ -751,13 +709,13 @@ (reduce only-inst-cb changes children-inst) :else - (if (cph/is-main-of? child-main child-inst) + (if (ctk/is-main-of? child-main child-inst) (recur (next children-inst) (next children-main) (both-cb changes child-inst child-main)) - (let [child-inst' (d/seek #(cph/is-main-of? child-main %) children-inst) - child-main' (d/seek #(cph/is-main-of? % child-inst) children-main)] + (let [child-inst' (d/seek #(ctk/is-main-of? child-main %) children-inst) + child-main' (d/seek #(ctk/is-main-of? % child-inst) children-main)] (cond (nil? child-inst') (recur children-inst @@ -785,8 +743,8 @@ (defn- add-shape-to-instance [changes component-shape index component container root-instance root-main omit-touched? set-remote-synced?] (log/info :msg (str "ADD [P] " (:name component-shape))) - (let [component-parent-shape (cph/get-shape component (:parent-id component-shape)) - parent-shape (d/seek #(cph/is-main-of? component-parent-shape %) + (let [component-parent-shape (ctn/get-shape component (:parent-id component-shape)) + parent-shape (d/seek #(ctk/is-main-of? component-parent-shape %) (cph/get-children-with-self (:objects container) (:id root-instance))) all-parents (into [(:id parent-shape)] @@ -811,7 +769,7 @@ original-shape) [_ new-shapes _] - (cph/clone-object component-shape + (ctst/clone-object component-shape (:id parent-shape) (get component :objects) update-new-shape @@ -853,8 +811,8 @@ (defn- add-shape-to-main [changes shape index component page root-instance root-main] (log/info :msg (str "ADD [C] " (:name shape))) - (let [parent-shape (cph/get-shape page (:parent-id shape)) - component-parent-shape (d/seek #(cph/is-main-of? % parent-shape) + (let [parent-shape (ctn/get-shape page (:parent-id shape)) + component-parent-shape (d/seek #(ctk/is-main-of? % parent-shape) (cph/get-children-with-self (:objects component) (:id root-main))) all-parents (into [(:id component-parent-shape)] @@ -873,7 +831,7 @@ original-shape)) [_new-shape new-shapes updated-shapes] - (cph/clone-object shape + (ctst/clone-object shape (:id component-parent-shape) (get page :objects) update-new-shape @@ -980,7 +938,7 @@ index-before " -> " index-after)) - (let [parent (cph/get-shape container (:parent-id shape)) + (let [parent (ctn/get-shape container (:parent-id shape)) changes' (-> changes (update :redo-changes conj (make-change diff --git a/frontend/src/app/main/data/workspace/path/drawing.cljs b/frontend/src/app/main/data/workspace/path/drawing.cljs index ff61c094a..fe2f4f55e 100644 --- a/frontend/src/app/main/data/workspace/path/drawing.cljs +++ b/frontend/src/app/main/data/workspace/path/drawing.cljs @@ -8,10 +8,10 @@ (:require [app.common.geom.point :as gpt] [app.common.geom.shapes.path :as upg] - [app.common.pages.helpers :as cph] [app.common.path.commands :as upc] [app.common.path.shapes-to-path :as upsp] [app.common.spec :as us] + [app.common.types.shape-tree :as ctt] [app.main.data.workspace.changes :as dch] [app.main.data.workspace.drawing.common :as dwdc] [app.main.data.workspace.edition :as dwe] @@ -258,7 +258,7 @@ (let [objects (wsh/lookup-page-objects state) content (get-in state [:workspace-drawing :object :content] []) position (get-in content [0 :params] nil) - frame-id (cph/frame-id-by-position objects position)] + frame-id (ctt/frame-id-by-position objects position)] (-> state (assoc-in [:workspace-drawing :object :frame-id] frame-id)))))) diff --git a/frontend/src/app/main/data/workspace/persistence.cljs b/frontend/src/app/main/data/workspace/persistence.cljs index 445f5001c..1d1e1cc36 100644 --- a/frontend/src/app/main/data/workspace/persistence.cljs +++ b/frontend/src/app/main/data/workspace/persistence.cljs @@ -16,12 +16,15 @@ [app.config :as cf] [app.main.data.dashboard :as dd] [app.main.data.fonts :as df] + [app.main.data.modal :as modal] [app.main.data.workspace.changes :as dch] [app.main.data.workspace.state-helpers :as wsh] [app.main.data.workspace.thumbnails :as dwt] + [app.main.features :as features] [app.main.repo :as rp] [app.main.store :as st] [app.util.http :as http] + [app.util.i18n :as i18n :refer [tr]] [app.util.router :as rt] [app.util.time :as dt] [beicon.core :as rx] @@ -124,8 +127,7 @@ (rx/map persist-synchronous-changes) (rx/take-until (rx/delay 100 stoper)) (rx/finalize (fn [] - (log/debug :hint "finalize persistence: synchronous save loop")))) - ))))) + (log/debug :hint "finalize persistence: synchronous save loop"))))))))) (defn persist-changes [file-id changes] @@ -134,12 +136,14 @@ (ptk/reify ::persist-changes ptk/WatchEvent (watch [_ state _] - (let [sid (:session-id state) - file (get state :workspace-file) - params {:id (:id file) - :revn (:revn file) - :session-id sid - :changes-with-metadata (into [] changes)}] + (let [components-v2 (features/active-feature? state :components-v2) + sid (:session-id state) + file (get state :workspace-file) + params {:id (:id file) + :revn (:revn file) + :session-id sid + :changes-with-metadata (into [] changes) + :components-v2 components-v2}] (when (= file-id (:id params)) (->> (rp/mutation :update-file params) @@ -175,13 +179,15 @@ (ptk/reify ::persist-synchronous-changes ptk/WatchEvent (watch [_ state _] - (let [sid (:session-id state) + (let [components-v2 (features/active-feature? state :components-v2) + sid (:session-id state) file (get-in state [:workspace-libraries file-id]) params {:id (:id file) :revn (:revn file) :session-id sid - :changes changes}] + :changes changes + :components-v2 components-v2}] (when (:id params) (->> (rp/mutation :update-file params) @@ -261,8 +267,9 @@ (ptk/reify ::fetch-bundle ptk/WatchEvent (watch [_ state _] - (let [share-id (-> state :viewer-local :share-id)] - (->> (rx/zip (rp/query :file-raw {:id file-id}) + (let [share-id (-> state :viewer-local :share-id) + components-v2 (features/active-feature? state :components-v2)] + (->> (rx/zip (rp/query :file-raw {:id file-id :components-v2 components-v2}) (rp/query :team-users {:file-id file-id}) (rp/query :project {:id project-id}) (rp/query :file-libraries {:file-id file-id}) @@ -276,8 +283,16 @@ :file-comments-users file-comments-users})) (rx/mapcat (fn [{:keys [project] :as bundle}] (rx/of (ptk/data-event ::bundle-fetched bundle) - (df/load-team-fonts (:team-id project)))))))))) - + (df/load-team-fonts (:team-id project))))) + (rx/catch (fn [err] + (if (and (= (:type err) :restriction) + (= (:code err) :feature-disabled)) + (let [team-id (:current-team-id state)] + (rx/of (modal/show + {:type :alert + :message (tr "errors.components-v2") + :on-accept #(st/emit! (rt/nav :dashboard-projects {:team-id team-id}))}))) + (rx/throw err))))))))) ;; --- Helpers diff --git a/frontend/src/app/main/data/workspace/selection.cljs b/frontend/src/app/main/data/workspace/selection.cljs index dbc2a68ba..f26c3400e 100644 --- a/frontend/src/app/main/data/workspace/selection.cljs +++ b/frontend/src/app/main/data/workspace/selection.cljs @@ -15,6 +15,7 @@ [app.common.pages.helpers :as cph] [app.common.spec :as us] [app.common.types.page :as ctp] + [app.common.types.shape-tree :as ctt] [app.common.types.shape.interactions :as ctsi] [app.common.uuid :as uuid] [app.main.data.modal :as md] @@ -26,7 +27,6 @@ [app.main.refs :as refs] [app.main.streams :as ms] [app.main.worker :as uw] - [app.util.names :as un] [beicon.core :as rx] [cljs.spec.alpha :as s] [clojure.set :as set] @@ -284,7 +284,7 @@ move to the desired position, and recalculate parents and frames as needed." [all-objects page ids delta it] (let [shapes (map (d/getf all-objects) ids) - unames (volatile! (un/retrieve-used-names (:objects page))) + unames (volatile! (ctt/retrieve-used-names (:objects page))) update-unames! (fn [new-name] (vswap! unames conj new-name)) all-ids (reduce #(into %1 (cons %2 (cph/get-children-ids all-objects %2))) (d/ordered-set) ids) ids-map (into {} (map #(vector % (uuid/next))) all-ids) @@ -319,7 +319,7 @@ (defn- prepare-duplicate-frame-change [changes objects page unames update-unames! ids-map obj delta] (let [new-id (ids-map (:id obj)) - frame-name (un/generate-unique-name @unames (:name obj)) + frame-name (ctt/generate-unique-name @unames (:name obj)) _ (update-unames! frame-name) new-frame (-> obj @@ -354,7 +354,7 @@ (if (some? obj) (let [new-id (ids-map (:id obj)) parent-id (or parent-id frame-id) - name (un/generate-unique-name @unames (:name obj)) + name (ctt/generate-unique-name @unames (:name obj)) _ (update-unames! name) new-obj (-> obj @@ -395,7 +395,7 @@ (let [update-flows (fn [flows] (reduce (fn [flows frame] - (let [name (un/generate-unique-name @unames "Flow-1") + (let [name (ctt/generate-unique-name @unames "Flow-1") _ (vswap! unames conj name) new-flow {:id (uuid/next) :name name diff --git a/frontend/src/app/main/data/workspace/shapes.cljs b/frontend/src/app/main/data/workspace/shapes.cljs index 28adef987..d6022f704 100644 --- a/frontend/src/app/main/data/workspace/shapes.cljs +++ b/frontend/src/app/main/data/workspace/shapes.cljs @@ -9,33 +9,33 @@ [app.common.data :as d] [app.common.data.macros :as dm] [app.common.geom.proportions :as gpr] - [app.common.pages :as cp] [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.page :as ctp] + [app.common.types.shape :as cts] + [app.common.types.shape-tree :as ctst] + [app.common.types.shape.interactions :as ctsi] [app.common.uuid :as uuid] [app.main.data.workspace.changes :as dch] [app.main.data.workspace.edition :as dwe] [app.main.data.workspace.selection :as dws] [app.main.data.workspace.shape-layout :as dwsl] [app.main.data.workspace.state-helpers :as wsh] + [app.main.features :as features] [app.main.streams :as ms] - [app.util.names :as un] [beicon.core :as rx] [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)) @@ -84,8 +84,8 @@ id (or (:id attrs) (uuid/next)) name (-> objects - (un/retrieve-used-names) - (un/generate-unique-name (:name attrs))) + (ctst/retrieve-used-names) + (ctst/generate-unique-name (:name attrs))) shape (make-new-shape (assoc attrs :id id :name name) @@ -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) @@ -138,13 +138,17 @@ (ptk/reify ::delete-shapes ptk/WatchEvent (watch [it state _] - (let [page-id (:current-page-id state) - objects (wsh/lookup-page-objects state page-id) - page (wsh/lookup-page state page-id) + (let [file-id (:current-file-id state) + page-id (:current-page-id state) + file (wsh/get-file state file-id) + page (wsh/lookup-page state page-id) + objects (wsh/lookup-page-objects state page-id) ids (cph/clean-loops objects ids) lookup (d/getf objects) + components-v2 (features/active-feature? state :components-v2) + groups-to-unmask (reduce (fn [group-ids id] ;; When the shape to delete is the mask of a masked group, @@ -164,7 +168,7 @@ ;; If any of the deleted shapes is the destination of ;; some interaction, this must be deleted, too. (let [interactions (:interactions shape)] - (some #(and (csi/has-destination %) + (some #(and (ctsi/has-destination %) (contains? ids (:destination %))) interactions))) (vals objects)) @@ -215,9 +219,22 @@ ;; Any parent whose children are all deleted, must be deleted too. (into (d/ordered-set) (find-all-empty-parents #{})) + components-to-delete + (if components-v2 + (reduce (fn [components id] + (let [shape (get objects id)] + (if (and (= (:component-file shape) file-id) ;; Main instances should exist only in local file + (:main-instance? shape)) ;; but check anyway + (conj components (:component-id shape)) + components))) + [] + (into ids all-children)) + []) + changes (-> (pcb/empty-changes it page-id) (pcb/with-page page) (pcb/with-objects objects) + (pcb/with-library-data file) (pcb/set-page-option :guides guides) (pcb/remove-objects all-children) (pcb/remove-objects ids) @@ -231,13 +248,18 @@ (d/update-when shape :interactions (fn [interactions] (into [] - (remove #(and (csi/has-destination %) + (remove #(and (ctsi/has-destination %) (contains? ids (:destination %)))) interactions))))) (cond-> (seq starting-flows) (pcb/update-page-option :flows (fn [flows] (->> (map :id starting-flows) - (reduce csp/remove-flow flows))))))] + (reduce ctp/remove-flow flows)))))) + + changes (reduce (fn [changes component-id] + (pcb/delete-component changes component-id)) + changes + components-to-delete)] (rx/of (dch/commit-changes changes) (dwsl/update-layout-positions all-parents)))))) @@ -260,10 +282,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/data/workspace/svg_upload.cljs b/frontend/src/app/main/data/workspace/svg_upload.cljs index 3c830bd59..2f3e5040e 100644 --- a/frontend/src/app/main/data/workspace/svg_upload.cljs +++ b/frontend/src/app/main/data/workspace/svg_upload.cljs @@ -11,17 +11,16 @@ [app.common.geom.matrix :as gmt] [app.common.geom.point :as gpt] [app.common.geom.shapes :as gsh] - [app.common.pages :as cp] [app.common.pages.changes-builder :as pcb] - [app.common.pages.helpers :as cph] [app.common.spec :refer [max-safe-int min-safe-int]] + [app.common.types.shape :as cts] + [app.common.types.shape-tree :as ctt] [app.common.uuid :as uuid] [app.main.data.workspace.changes :as dch] [app.main.data.workspace.selection :as dws] [app.main.data.workspace.shapes :as dwsh] [app.main.data.workspace.state-helpers :as wsh] [app.util.color :as uc] - [app.util.names :as un] [app.util.path.parser :as upp] [app.util.svg :as usvg] [beicon.core :as rx] @@ -183,7 +182,7 @@ (assoc :svg-attrs attrs) (assoc :svg-viewbox (-> (select-keys svg-data [:width :height]) (assoc :x offset-x :y offset-y))) - (cp/setup-rect-selrect)))) + (cts/setup-rect-selrect)))) (defn create-svg-root [frame-id svg-data] (let [{:keys [name x y width height offset-x offset-y]} svg-data] @@ -195,7 +194,7 @@ :height height :x (+ x offset-x) :y (+ y offset-y)} - (cp/setup-rect-selrect) + (cts/setup-rect-selrect) (assoc :svg-attrs (-> (:attrs svg-data) (dissoc :viewBox :xmlns) (d/without-keys usvg/inheritable-props)))))) @@ -215,7 +214,7 @@ (assoc :svg-attrs (d/without-keys attrs usvg/inheritable-props)) (assoc :svg-viewbox (-> (select-keys svg-data [:width :height]) (assoc :x offset-x :y offset-y))) - (cp/setup-rect-selrect)))) + (cts/setup-rect-selrect)))) (defn create-path-shape [name frame-id svg-data {:keys [attrs] :as data}] (when (and (contains? attrs :d) (seq (:d attrs))) @@ -360,7 +359,7 @@ (let [{:keys [tag attrs hidden]} element-data attrs (usvg/format-styles attrs) element-data (cond-> element-data (map? element-data) (assoc :attrs attrs)) - name (un/generate-unique-name unames (or (:id attrs) (tag->name tag))) + name (ctt/generate-unique-name unames (or (:id attrs) (tag->name tag))) att-refs (usvg/find-attr-references attrs) references (usvg/find-def-references (:defs svg-data) att-refs) @@ -437,17 +436,17 @@ (try (let [page-id (:current-page-id state) objects (wsh/lookup-page-objects state page-id) - frame-id (cph/frame-id-by-position objects position) + frame-id (ctt/frame-id-by-position objects position) selected (wsh/lookup-selected state) [vb-x vb-y vb-width vb-height] (svg-dimensions svg-data) x (- x vb-x (/ vb-width 2)) y (- y vb-y (/ vb-height 2)) - unames (un/retrieve-used-names objects) + unames (ctt/retrieve-used-names objects) svg-name (->> (str/replace (:name svg-data) ".svg" "") - (un/generate-unique-name unames)) + (ctt/generate-unique-name unames)) svg-data (-> svg-data (assoc :x x diff --git a/frontend/src/app/main/data/workspace/transforms.cljs b/frontend/src/app/main/data/workspace/transforms.cljs index e8e9268fa..34b2ef920 100644 --- a/frontend/src/app/main/data/workspace/transforms.cljs +++ b/frontend/src/app/main/data/workspace/transforms.cljs @@ -16,6 +16,7 @@ [app.common.pages.common :as cpc] [app.common.pages.helpers :as cph] [app.common.spec :as us] + [app.common.types.shape-tree :as ctt] [app.main.data.workspace.changes :as dch] [app.main.data.workspace.collapse :as dwc] [app.main.data.workspace.guides :as dwg] @@ -752,7 +753,7 @@ (let [position @ms/mouse-position page-id (:current-page-id state) objects (wsh/lookup-page-objects state page-id) - frame-id (cph/frame-id-by-position objects position) + frame-id (ctt/frame-id-by-position objects position) moving-shapes (->> ids diff --git a/frontend/src/app/main/ui/features.cljs b/frontend/src/app/main/features.cljs similarity index 68% rename from frontend/src/app/main/ui/features.cljs rename to frontend/src/app/main/features.cljs index 30f0ebb59..5239bdd39 100644 --- a/frontend/src/app/main/ui/features.cljs +++ b/frontend/src/app/main/features.cljs @@ -4,10 +4,11 @@ ;; ;; Copyright (c) UXBOX Labs SL -(ns app.main.ui.features +(ns app.main.features (:require [app.common.data :as d] [app.common.logging :as log] + [app.config :as cfg] [app.main.store :as st] [okulary.core :as l] [potok.core :as ptk] @@ -15,9 +16,9 @@ (log/set-level! :debug) -(def features-list #{:auto-layout}) +(def features-list #{:auto-layout :components-v2}) -(defn toggle-feature +(defn- toggle-feature [feature] (ptk/reify ::toggle-feature ptk/UpdateEvent @@ -27,7 +28,6 @@ :result (if (not (contains? (:features state) feature)) "enabled" "disabled")) - (-> state (update :features (fn [features] @@ -41,6 +41,13 @@ (assert (contains? features-list feature) "Not supported feature") (st/emit! (toggle-feature feature))) +(defn active-feature? + ([feature] + (active-feature? @st/state feature)) + ([state feature] + (assert (contains? features-list feature) "Not supported feature") + (contains? (get state :features) feature))) + (def features (l/derived :features st/state)) @@ -55,8 +62,14 @@ active-feature? (mf/deref active-feature-ref)] active-feature?)) -;; By default the features are active in local environments -(when *assert* - ;; Activate all features in local environment - (doseq [f features-list] - (toggle-feature! f))) +;; Read initial enabled features from config, if set +(if-let [enabled-features @cfg/features] + (doseq [f enabled-features] + (toggle-feature! f)) + (when *assert* + ;; By default, all features disabled, except in development + ;; environment, that are enabled except components-v2 + (doseq [f features-list] + (when (not= f :components-v2) + (toggle-feature! f))))) + diff --git a/frontend/src/app/main/refs.cljs b/frontend/src/app/main/refs.cljs index 46302a4a1..35edbc26c 100644 --- a/frontend/src/app/main/refs.cljs +++ b/frontend/src/app/main/refs.cljs @@ -10,6 +10,7 @@ [app.common.data :as d] [app.common.data.macros :as dm] [app.common.pages.helpers :as cph] + [app.common.types.shape-tree :as ctt] [app.main.data.workspace.state-helpers :as wsh] [app.main.store :as st] [okulary.core :as l])) @@ -284,7 +285,7 @@ (l/derived :options workspace-page)) (def workspace-frames - (l/derived cph/get-frames workspace-page-objects =)) + (l/derived ctt/get-frames workspace-page-objects =)) (def workspace-editor (l/derived :workspace-editor st/state)) diff --git a/frontend/src/app/main/render.cljs b/frontend/src/app/main/render.cljs index 240a44201..66b8ee2c4 100644 --- a/frontend/src/app/main/render.cljs +++ b/frontend/src/app/main/render.cljs @@ -21,6 +21,7 @@ [app.common.geom.shapes.bounds :as gsb] [app.common.math :as mth] [app.common.pages.helpers :as cph] + [app.common.types.shape-tree :as ctst] [app.config :as cfg] [app.main.fonts :as fonts] [app.main.ui.context :as muc] @@ -61,7 +62,7 @@ (defn- calculate-dimensions [objects] (let [bounds - (->> (cph/get-root-objects objects) + (->> (ctst/get-root-objects objects) (map (partial gsb/get-object-bounds objects)) (gsh/join-rects))] (-> bounds diff --git a/frontend/src/app/main/ui/alert.cljs b/frontend/src/app/main/ui/alert.cljs new file mode 100644 index 000000000..1dcf78640 --- /dev/null +++ b/frontend/src/app/main/ui/alert.cljs @@ -0,0 +1,78 @@ +;; 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.main.ui.alert + (:require + [app.main.data.modal :as modal] + [app.main.store :as st] + [app.main.ui.icons :as i] + [app.util.dom :as dom] + [app.util.i18n :as i18n :refer [tr t]] + [app.util.keyboard :as k] + [goog.events :as events] + [rumext.alpha :as mf]) + (:import goog.events.EventType)) + +(mf/defc alert-dialog + {::mf/register modal/components + ::mf/register-as :alert} + [{:keys [message + scd-message + title + on-accept + hint + accept-label + accept-style] :as props}] + (let [locale (mf/deref i18n/locale) + + on-accept (or on-accept identity) + message (or message (t locale "ds.alert-title")) + accept-label (or accept-label (tr "ds.alert-ok")) + accept-style (or accept-style :danger) + title (or title (t locale "ds.alert-title")) + + accept-fn + (mf/use-callback + (fn [event] + (dom/prevent-default event) + (st/emit! (modal/hide)) + (on-accept props)))] + + (mf/with-effect + (letfn [(on-keydown [event] + (when (k/enter? event) + (dom/prevent-default event) + (dom/stop-propagation event) + (st/emit! (modal/hide)) + (on-accept props)))] + (->> (events/listen js/document EventType.KEYDOWN on-keydown) + (partial events/unlistenByKey)))) + + [:div.modal-overlay + [:div.modal-container.alert-dialog + [:div.modal-header + [:div.modal-header-title + [:h2 title]] + [:div.modal-close-button + {:on-click accept-fn} i/close]] + + [:div.modal-content + (when (and (string? message) (not= message "")) + [:h3 message]) + (when (and (string? scd-message) (not= scd-message "")) + [:h3 scd-message]) + (when (string? hint) + [:p hint])] + + [:div.modal-footer + [:div.action-buttons + [:input.accept-button + {:class (dom/classnames + :danger (= accept-style :danger) + :primary (= accept-style :primary)) + :type "button" + :value accept-label + :on-click accept-fn}]]]]])) diff --git a/frontend/src/app/main/ui/components/shape_icon.cljs b/frontend/src/app/main/ui/components/shape_icon.cljs index 0a54feeae..21b5e1e32 100644 --- a/frontend/src/app/main/ui/components/shape_icon.cljs +++ b/frontend/src/app/main/ui/components/shape_icon.cljs @@ -11,7 +11,7 @@ (mf/defc element-icon - [{:keys [shape] :as props}] + [{:keys [shape main-instance?] :as props}] (case (:type shape) :frame i/artboard :image i/image @@ -21,7 +21,9 @@ :rect i/box :text i/text :group (if (some? (:component-id shape)) - i/component + (if main-instance? + i/component + i/component-copy) (if (:masked-group? shape) i/mask i/folder)) diff --git a/frontend/src/app/main/ui/context.cljs b/frontend/src/app/main/ui/context.cljs index 34a3518c9..8ec9e69fa 100644 --- a/frontend/src/app/main/ui/context.cljs +++ b/frontend/src/app/main/ui/context.cljs @@ -21,6 +21,8 @@ (def current-project-id (mf/create-context nil)) (def current-page-id (mf/create-context nil)) (def current-file-id (mf/create-context nil)) +(def libraries (mf/create-context nil)) (def scroll-ctx (mf/create-context nil)) (def active-frames-ctx (mf/create-context nil)) -(def render-thumbnails (mf/create-context nil)) +(def render-thumbnails (mf/create-context nil)) +(def components-v2 (mf/create-context nil)) diff --git a/frontend/src/app/main/ui/dashboard/export.cljs b/frontend/src/app/main/ui/dashboard/export.cljs index 58f8d957d..998e55803 100644 --- a/frontend/src/app/main/ui/dashboard/export.cljs +++ b/frontend/src/app/main/ui/dashboard/export.cljs @@ -8,6 +8,7 @@ (:require [app.common.data :as d] [app.main.data.modal :as modal] + [app.main.features :as features] [app.main.store :as st] [app.main.ui.icons :as i] [app.main.worker :as uw] @@ -56,6 +57,8 @@ :files (->> files (mapv #(assoc % :loading? true)))}) selected-option (mf/use-state :all) + components-v2 (features/use-feature :components-v2) + start-export (fn [] (swap! state assoc :status :exporting) @@ -64,7 +67,7 @@ :team-id team-id :export-type @selected-option :files files - }) + :components-v2 components-v2}) (rx/delay-emit 1000) (rx/subs (fn [msg] diff --git a/frontend/src/app/main/ui/dashboard/file_menu.cljs b/frontend/src/app/main/ui/dashboard/file_menu.cljs index 7903f591a..3091904e1 100644 --- a/frontend/src/app/main/ui/dashboard/file_menu.cljs +++ b/frontend/src/app/main/ui/dashboard/file_menu.cljs @@ -92,19 +92,26 @@ on-delete (fn [event] (dom/stop-propagation event) - (if multi? - (st/emit! (modal/show - {:type :confirm - :title (tr "modals.delete-file-multi-confirm.title" file-count) - :message (tr "modals.delete-file-multi-confirm.message" file-count) - :accept-label (tr "modals.delete-file-multi-confirm.accept" file-count) - :on-accept delete-fn})) - (st/emit! (modal/show - {:type :confirm - :title (tr "modals.delete-file-confirm.title") - :message (tr "modals.delete-file-confirm.message") - :accept-label (tr "modals.delete-file-confirm.accept") - :on-accept delete-fn})))) + (if (:is-shared file) + (do (st/emit! (dd/fetch-library-using-files file)) + (st/emit! (modal/show + {:type :delete-shared + :origin :delete + :on-accept delete-fn}))) + + (if multi? + (st/emit! (modal/show + {:type :confirm + :title (tr "modals.delete-file-multi-confirm.title" file-count) + :message (tr "modals.delete-file-multi-confirm.message" file-count) + :accept-label (tr "modals.delete-file-multi-confirm.accept" file-count) + :on-accept delete-fn})) + (st/emit! (modal/show + {:type :confirm + :title (tr "modals.delete-file-confirm.title") + :message (tr "modals.delete-file-confirm.message") + :accept-label (tr "modals.delete-file-confirm.accept") + :on-accept delete-fn}))))) on-move-success (fn [team-id project-id] @@ -148,13 +155,10 @@ (fn [event] (dom/prevent-default event) (dom/stop-propagation event) + (st/emit! (dd/fetch-library-using-files file)) (st/emit! (modal/show - {:type :confirm - :message "" - :title (tr "modals.remove-shared-confirm.message" (:name file)) - :hint (tr "modals.remove-shared-confirm.hint") - :cancel-label :omit - :accept-label (tr "modals.remove-shared-confirm.accept") + {:type :delete-shared + :origin :unpublish :on-accept del-shared}))) on-export-files @@ -233,7 +237,7 @@ (when (or (seq current-projects) (seq other-teams)) [(tr "dashboard.move-to") nil sub-options "file-move-to"]) (if (:is-shared file) - [(tr "dashboard.remove-shared") on-del-shared nil "file-del-shared"] + [(tr "dashboard.unpublish-shared") on-del-shared nil "file-del-shared"] [(tr "dashboard.add-shared") on-add-shared nil "file-add-shared"]) [:separator] [(tr "dashboard.download-binary-file") on-export-binary-files nil "download-binary-file"] diff --git a/frontend/src/app/main/ui/dashboard/files.cljs b/frontend/src/app/main/ui/dashboard/files.cljs index 99678a6c3..251be9a69 100644 --- a/frontend/src/app/main/ui/dashboard/files.cljs +++ b/frontend/src/app/main/ui/dashboard/files.cljs @@ -127,5 +127,6 @@ [:section.dashboard-container [:& grid {:project project :files files - :on-create-clicked on-create-clicked}]]])) + :on-create-clicked on-create-clicked + :origin :files}]]])) diff --git a/frontend/src/app/main/ui/dashboard/grid.cljs b/frontend/src/app/main/ui/dashboard/grid.cljs index f4057a60a..de1ac3b5c 100644 --- a/frontend/src/app/main/ui/dashboard/grid.cljs +++ b/frontend/src/app/main/ui/dashboard/grid.cljs @@ -10,6 +10,7 @@ [app.common.math :as mth] [app.main.data.dashboard :as dd] [app.main.data.messages :as dm] + [app.main.features :as features] [app.main.fonts :as fonts] [app.main.refs :as refs] [app.main.store :as st] @@ -36,9 +37,11 @@ (defn ask-for-thumbnail "Creates some hooks to handle the files thumbnails cache" [file] - (wrk/ask! {:cmd :thumbnails/generate - :revn (:revn file) - :file-id (:id file)})) + (let [components-v2 (features/active-feature? :components-v2)] + (wrk/ask! {:cmd :thumbnails/generate + :revn (:revn file) + :file-id (:id file) + :components-v2 components-v2}))) (mf/defc grid-item-thumbnail {::mf/wrap [mf/memo]} @@ -72,12 +75,13 @@ (mf/defc grid-item {:wrap [mf/memo]} - [{:keys [file navigate?] :as props}] + [{:keys [file navigate? origin] :as props}] (let [file-id (:id file) local (mf/use-state {:menu-open false :menu-pos nil :edition false}) selected-files (mf/deref refs/dashboard-selected-files) + dashboard-local (mf/deref refs/dashboard-local) item-ref (mf/use-ref) menu-ref (mf/use-ref) selected? (contains? selected-files file-id) @@ -202,10 +206,12 @@ :top (:y (:menu-pos @local)) :navigate? navigate? :on-edit on-edit - :on-menu-close on-menu-close}])]]])) + :on-menu-close on-menu-close + :origin origin + :dashboard-local dashboard-local}])]]])) (mf/defc grid - [{:keys [files project on-create-clicked] :as props}] + [{:keys [files project on-create-clicked origin] :as props}] (let [dragging? (mf/use-state false) project-id (:id project) @@ -265,7 +271,8 @@ [:& grid-item {:file item :key (:id item) - :navigate? true}])] + :navigate? true + :origin origin}])] :else [:& empty-placeholder {:default? (:is-default project) @@ -273,7 +280,7 @@ :project project}])])) (mf/defc line-grid-row - [{:keys [files selected-files on-load-more dragging?] :as props}] + [{:keys [files selected-files on-load-more dragging? origin] :as props}] (let [rowref (mf/use-ref) width (mf/use-state nil) @@ -319,7 +326,8 @@ :file item :selected-files selected-files :key (:id item) - :navigate? false}]) + :navigate? false + :origin origin}]) (when (and (> limit 0) (> (count files) limit)) [:div.grid-item.placeholder {:on-click on-load-more} @@ -328,7 +336,7 @@ (tr "dashboard.show-all-files")]])])) (mf/defc line-grid - [{:keys [project team files on-load-more on-create-clicked] :as props}] + [{:keys [project team files on-load-more on-create-clicked origin] :as props}] (let [dragging? (mf/use-state false) project-id (:id project) team-id (:id team) @@ -412,7 +420,8 @@ :team-id team-id :selected-files selected-files :on-load-more on-load-more - :dragging? @dragging?}] + :dragging? @dragging? + :origin origin}] :else [:& empty-placeholder {:dragging? @dragging? diff --git a/frontend/src/app/main/ui/dashboard/libraries.cljs b/frontend/src/app/main/ui/dashboard/libraries.cljs index 80245771e..df8e838fd 100644 --- a/frontend/src/app/main/ui/dashboard/libraries.cljs +++ b/frontend/src/app/main/ui/dashboard/libraries.cljs @@ -42,5 +42,6 @@ [:h1 (tr "dashboard.libraries-title")]]] [:section.dashboard-container [:& grid {:files files - :project default-project}]]])) + :project default-project + :origin :libraries}]]])) diff --git a/frontend/src/app/main/ui/dashboard/projects.cljs b/frontend/src/app/main/ui/dashboard/projects.cljs index d23ffc789..1622eb3f4 100644 --- a/frontend/src/app/main/ui/dashboard/projects.cljs +++ b/frontend/src/app/main/ui/dashboard/projects.cljs @@ -153,7 +153,8 @@ :team team :on-load-more on-nav :files files - :on-create-clicked (partial create-file "dashboard:empty-folder-placeholder")}]])) + :on-create-clicked (partial create-file "dashboard:empty-folder-placeholder") + :origin :project}]])) (def recent-files-ref (l/derived :dashboard-recent-files st/state)) diff --git a/frontend/src/app/main/ui/delete_shared.cljs b/frontend/src/app/main/ui/delete_shared.cljs new file mode 100644 index 000000000..d1fbe026a --- /dev/null +++ b/frontend/src/app/main/ui/delete_shared.cljs @@ -0,0 +1,117 @@ +;; 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.main.ui.delete-shared + (:require + [app.main.data.dashboard :as dd] + [app.main.data.modal :as modal] + [app.main.refs :as refs] + [app.main.store :as st] + [app.main.ui.icons :as i] + [app.util.dom :as dom] + [app.util.i18n :as i18n :refer [tr]] + [app.util.keyboard :as k] + [goog.events :as events] + [rumext.alpha :as mf]) + (:import goog.events.EventType)) + +(mf/defc delete-shared-dialog + {::mf/register modal/components + ::mf/register-as :delete-shared} + [{:keys [on-accept + on-cancel + accept-style + origin] :as props}] + (let [on-accept (or on-accept identity) + on-cancel (or on-cancel identity) + cancel-label (tr "labels.cancel") + accept-style (or accept-style :danger) + is-delete? (= origin :delete) + dashboard-local (mf/deref refs/dashboard-local) + files->shared (:files-with-shared dashboard-local) + count-files (count (keys files->shared)) + title (if is-delete? + (tr "modals.delete-shared-confirm.title") + (tr "modals.unpublish-shared-confirm.title")) + message (if is-delete? + (tr "modals.delete-shared-confirm.message") + (tr "modals.unpublish-shared-confirm.message")) + + accept-label (if is-delete? + (tr "modals.delete-shared-confirm.accept") + (tr "modals.unpublish-shared-confirm.accept")) + scd-message (if is-delete? + (tr "modals.delete-shared-confirm.scd-message" (i18n/c count-files)) + (tr "modals.unpublish-shared-confirm.scd-message" (i18n/c count-files))) + hint (if is-delete? + "" + (tr "modals.unpublish-shared-confirm.hint" (i18n/c count-files))) + + accept-fn + (mf/use-callback + (fn [event] + (dom/prevent-default event) + (st/emit! (modal/hide)) + (on-accept props))) + + cancel-fn + (mf/use-callback + (fn [event] + (dom/prevent-default event) + (st/emit! (modal/hide)) + (on-cancel props)))] + + (mf/with-effect + (letfn [(on-keydown [event] + (when (k/enter? event) + (dom/prevent-default event) + (dom/stop-propagation event) + (st/emit! (modal/hide)) + (on-accept props)))] + (->> (events/listen js/document EventType.KEYDOWN on-keydown) + (partial events/unlistenByKey))) + #(st/emit! (dd/clean-temp-shared))) + + [:div.modal-overlay + [:div.modal-container.confirm-dialog + [:div.modal-header + [:div.modal-header-title + [:h2 title]] + [:div.modal-close-button + {:on-click cancel-fn} i/close]] + + [:div.modal-content.delete-shared + (when (and (string? message) (not= message "")) + [:h3 message]) + + (when (> (count files->shared) 0) + [:* + [:div + (when (and (string? scd-message) (not= scd-message "")) + [:h3 scd-message]) + [:ul.file-list + (for [[id file] files->shared] + [:li.modal-item-element + {:key id} + [:span "- " (:name file)]])]] + (when (and (string? hint) (not= hint "")) + [:h3 hint])])] + + [:div.modal-footer + [:div.action-buttons + (when-not (= cancel-label :omit) + [:input.cancel-button + {:type "button" + :value cancel-label + :on-click cancel-fn}]) + + [:input.accept-button + {:class (dom/classnames + :danger (= accept-style :danger) + :primary (= accept-style :primary)) + :type "button" + :value accept-label + :on-click accept-fn}]]]]])) diff --git a/frontend/src/app/main/ui/icons.cljs b/frontend/src/app/main/ui/icons.cljs index b8232b043..ae5d38542 100644 --- a/frontend/src/app/main/ui/icons.cljs +++ b/frontend/src/app/main/ui/icons.cljs @@ -53,6 +53,7 @@ (def close (icon-xref :close)) (def code (icon-xref :code)) (def component (icon-xref :component)) +(def component-copy (icon-xref :component-copy)) (def copy (icon-xref :copy)) (def curve (icon-xref :curve)) (def cross (icon-xref :cross)) diff --git a/frontend/src/app/main/ui/workspace.cljs b/frontend/src/app/main/ui/workspace.cljs index 17b1fce61..18cc78118 100644 --- a/frontend/src/app/main/ui/workspace.cljs +++ b/frontend/src/app/main/ui/workspace.cljs @@ -11,6 +11,7 @@ [app.main.data.messages :as msg] [app.main.data.workspace :as dw] [app.main.data.workspace.persistence :as dwp] + [app.main.features :as features] [app.main.refs :as refs] [app.main.store :as st] [app.main.ui.context :as ctx] @@ -119,6 +120,8 @@ layout (mf/deref refs/workspace-layout) wglobal (mf/deref refs/workspace-global) + components-v2 (features/use-feature :components-v2) + background-color (:background-color wglobal)] ;; Setting the layout preset by its name @@ -145,23 +148,22 @@ [:& (mf/provider ctx/current-team-id) {:value (:team-id project)} [:& (mf/provider ctx/current-project-id) {:value (:id project)} [:& (mf/provider ctx/current-page-id) {:value page-id} - [:section#workspace {:style {:background-color background-color}} - (when (not (:hide-ui layout)) - [:& header {:file file - :page-id page-id - :project project - :layout layout}]) - - [:& context-menu] - - (if (and (and file project) - (:initialized file)) - [:& workspace-page {:key (dm/str "page-" page-id) - :page-id page-id - :file file - :wglobal wglobal - :layout layout}] - [:& workspace-loader])]]]]])) + [:& (mf/provider ctx/components-v2) {:value components-v2} + [:section#workspace {:style {:background-color background-color}} + (when (not (:hide-ui layout)) + [:& header {:file file + :page-id page-id + :project project + :layout layout}]) + [:& context-menu] + (if (and (and file project) + (:initialized file)) + [:& workspace-page {:key (dm/str "page-" page-id) + :page-id page-id + :file file + :wglobal wglobal + :layout layout}] + [:& workspace-loader])]]]]]])) diff --git a/frontend/src/app/main/ui/workspace/sidebar/assets.cljs b/frontend/src/app/main/ui/workspace/sidebar/assets.cljs index 8e408c060..e80065095 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/assets.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/assets.cljs @@ -188,7 +188,6 @@ (add-group % group-name))))) (st/emit! (dwu/commit-undo-transaction))) - (defn- on-drop-asset [event asset dragging? selected-assets selected-assets-full selected-assets-paths rename] (let [create-typed-assets-group (partial create-assets-group rename)] @@ -568,7 +567,7 @@ (on-assets-delete) (st/emit! (dwu/start-undo-transaction) (dwl/delete-component {:id (:component-id @state)}) - (dwl/sync-file file-id file-id) + (dwl/sync-file file-id file-id :components (:component-id @state)) (dwu/commit-undo-transaction))))) on-rename @@ -1120,7 +1119,7 @@ (on-assets-delete) (st/emit! (dwu/start-undo-transaction) (dwl/delete-color color) - (dwl/sync-file file-id file-id) + (dwl/sync-file file-id file-id :color (:id color)) (dwu/commit-undo-transaction))))) rename-color-clicked @@ -1762,7 +1761,7 @@ (on-assets-delete) (st/emit! (dwu/start-undo-transaction) (dwl/delete-typography (:id @state)) - (dwl/sync-file file-id file-id) + (dwl/sync-file file-id file-id :typographies (:id @state)) (dwu/commit-undo-transaction))))) editing-id (or (:rename-typography local-data) diff --git a/frontend/src/app/main/ui/workspace/sidebar/layers.cljs b/frontend/src/app/main/ui/workspace/sidebar/layers.cljs index 32b19d0fa..82d001351 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/layers.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/layers.cljs @@ -15,6 +15,7 @@ [app.main.refs :as refs] [app.main.store :as st] [app.main.ui.components.shape-icon :as si] + [app.main.ui.context :as ctx] [app.main.ui.hooks :as hooks] [app.main.ui.icons :as i] [app.util.dom :as dom] @@ -101,6 +102,11 @@ container? (or (cph/frame-shape? item) (cph/group-shape? item)) + components-v2 (mf/use-ctx ctx/components-v2) + main-instance? (if components-v2 + (:main-instance? item) + true) + toggle-collapse (mf/use-fn (mf/deps expanded?) @@ -244,7 +250,8 @@ [:div {:on-double-click #(do (dom/stop-propagation %) (dom/prevent-default %) (st/emit! dw/zoom-to-selected-shape))} - [:& si/element-icon {:shape item}]] + [:& si/element-icon {:shape item + :main-instance? main-instance?}]] [:& layer-name {:shape item :name-ref ref :on-start-edit #(reset! disable-drag true) @@ -444,7 +451,6 @@ (take (:num-items @filter-state)) filtered-objects-total)))) - handle-show-more (fn [] (when (<= (:num-items @filter-state) (count filtered-objects-total)) @@ -542,7 +548,6 @@ (when last-hidden-frame (dom/add-class! last-hidden-frame "sticky"))))] - [:div#layers.tool-window (if (d/not-empty? focus) [:div.tool-window-bar 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 0a28635ce..34e044ae9 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,22 +6,23 @@ (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.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]) +(def component-attrs [:component-id :component-file :shape-ref :main-instance?]) (mf/defc component-menu [{:keys [ids values shape-name] :as props}] (let [current-file-id (mf/use-ctx ctx/current-file-id) + components-v2 (mf/use-ctx ctx/components-v2) id (first ids) local (mf/use-state {:menu-open false}) @@ -29,6 +30,9 @@ component-id (:component-id values) library-id (:component-file values) show? (some? component-id) + main-instance? (if components-v2 + (:main-instance? values) + true) on-menu-click (mf/use-callback @@ -69,7 +73,9 @@ [:span (tr "workspace.options.component")]] [:div.element-set-content [:div.row-flex.component-row - i/component + (if main-instance? + i/component + i/component-copy) shape-name [:div.row-actions {:on-click on-menu-click} diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs index 18035e283..5efd82332 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs @@ -10,6 +10,7 @@ [app.common.data.macros :as dm] [app.common.pages.helpers :as cph] [app.common.types.page :as ctp] + [app.common.types.shape-tree :as ctt] [app.common.types.shape.interactions :as ctsi] [app.common.uuid :as uuid] [app.main.data.workspace :as dw] @@ -182,7 +183,7 @@ (let [objects (deref refs/workspace-page-objects) destination (get objects (:destination interaction)) - frames (mf/with-memo [objects] (cph/get-viewer-frames objects {:all-frames? (not= :navigate (:action-type interaction))})) + frames (mf/with-memo [objects] (ctt/get-viewer-frames objects {:all-frames? (not= :navigate (:action-type interaction))})) overlay-pos-type (:overlay-pos-type interaction) close-click-outside? (:close-click-outside interaction false) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/shapes/frame.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/shapes/frame.cljs index 6a6957c25..cbd5d7115 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/shapes/frame.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/shapes/frame.cljs @@ -6,8 +6,8 @@ (ns app.main.ui.workspace.sidebar.options.shapes.frame (:require + [app.main.features :as features] [app.main.refs :as refs] - [app.main.ui.features :as features] [app.main.ui.workspace.sidebar.options.menus.blur :refer [blur-menu]] [app.main.ui.workspace.sidebar.options.menus.constraints :refer [constraint-attrs constraints-menu]] [app.main.ui.workspace.sidebar.options.menus.fill :refer [fill-attrs-shape fill-menu]] diff --git a/frontend/src/app/main/ui/workspace/viewport/frame_grid.cljs b/frontend/src/app/main/ui/workspace/viewport/frame_grid.cljs index ece6b20b2..6f1691283 100644 --- a/frontend/src/app/main/ui/workspace/viewport/frame_grid.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/frame_grid.cljs @@ -9,7 +9,7 @@ [app.common.data :as d] [app.common.geom.shapes :as gsh] [app.common.math :as mth] - [app.common.pages.helpers :as cph] + [app.common.types.shape-tree :as ctst] [app.common.uuid :as uuid] [app.main.refs :as refs] [app.util.geom.grid :as gg] @@ -134,7 +134,7 @@ [:g.grid-display {:style {:pointer-events "none"}} (for [frame frames] (when (and (not (is-transform? frame)) - (not (cph/rotated-frame? frame)) + (not (ctst/rotated-frame? frame)) (or (empty? focus) (contains? focus (:id frame)))) [:& grid-display-frame {:key (str "grid-" (:id frame)) :zoom zoom diff --git a/frontend/src/app/main/ui/workspace/viewport/guides.cljs b/frontend/src/app/main/ui/workspace/viewport/guides.cljs index ed338e282..067db2303 100644 --- a/frontend/src/app/main/ui/workspace/viewport/guides.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/guides.cljs @@ -11,6 +11,7 @@ [app.common.geom.shapes :as gsh] [app.common.math :as mth] [app.common.pages.helpers :as cph] + [app.common.types.shape-tree :as ctst] [app.common.uuid :as uuid] [app.main.data.workspace :as dw] [app.main.refs :as refs] @@ -292,7 +293,7 @@ (when (or (nil? frame) (and (cph/root-frame? frame) - (not (cph/rotated-frame? frame)))) + (not (ctst/rotated-frame? frame)))) [:g.guide-area {:opacity (when frame-guide-outside? 0)} (when-not disabled-guides? (let [{:keys [x y width height]} (guide-area-axis pos vbox zoom frame axis)] diff --git a/frontend/src/app/main/ui/workspace/viewport/hooks.cljs b/frontend/src/app/main/ui/workspace/viewport/hooks.cljs index a6f488149..2909ab0b4 100644 --- a/frontend/src/app/main/ui/workspace/viewport/hooks.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/hooks.cljs @@ -10,6 +10,7 @@ [app.common.geom.shapes :as gsh] [app.common.pages :as cp] [app.common.pages.helpers :as cph] + [app.common.types.shape-tree :as ctt] [app.main.data.shortcuts :as dsc] [app.main.data.workspace :as dw] [app.main.data.workspace.path.shortcuts :as psc] @@ -183,7 +184,7 @@ ids (into (d/ordered-set) - (cph/sort-z-index objects ids {:bottom-frames? mod?})) + (ctt/sort-z-index objects ids {:bottom-frames? mod?})) grouped? (fn [id] (contains? #{:group :bool} (get-in objects [id :type]))) @@ -218,7 +219,7 @@ (let [root-frame-ids (mf/use-memo (mf/deps objects) - #(cph/get-root-shapes-ids objects)) + #(ctt/get-root-shapes-ids objects)) modifiers (select-keys modifiers root-frame-ids)] (sfd/use-dynamic-modifiers objects globals/document modifiers))) @@ -229,7 +230,7 @@ (defn setup-active-frames [objects hover-ids selected active-frames zoom transform vbox] - (let [all-frames (mf/use-memo (mf/deps objects) #(cph/get-root-frames-ids objects)) + (let [all-frames (mf/use-memo (mf/deps objects) #(ctt/get-root-frames-ids objects)) selected-frames (mf/use-memo (mf/deps selected) #(->> all-frames (filter selected))) xf-selected-frame (comp (remove cph/root-frame?) diff --git a/frontend/src/app/main/ui/workspace/viewport/selection.cljs b/frontend/src/app/main/ui/workspace/viewport/selection.cljs index 4a1e4f7de..2f774d0d2 100644 --- a/frontend/src/app/main/ui/workspace/viewport/selection.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/selection.cljs @@ -11,7 +11,7 @@ [app.common.geom.matrix :as gmt] [app.common.geom.point :as gpt] [app.common.geom.shapes :as gsh] - [app.common.pages :as cp] + [app.common.types.shape :as cts] [app.main.data.workspace :as dw] [app.main.refs :as refs] [app.main.store :as st] @@ -343,7 +343,7 @@ #(->> shapes (map gsh/transform-shape) (gsh/selection-rect) - (cp/setup-shape))) + (cts/setup-shape))) on-resize (fn [current-position _initial-position event] (when (dom/left-mouse? event) @@ -371,7 +371,7 @@ #(->> shapes (map gsh/transform-shape) (gsh/selection-rect) - (cp/setup-shape)))] + (cts/setup-shape)))] [:& controls-selection {:shape shape diff --git a/frontend/src/app/main/ui/workspace/viewport/widgets.cljs b/frontend/src/app/main/ui/workspace/viewport/widgets.cljs index 56725968f..1ae92aa12 100644 --- a/frontend/src/app/main/ui/workspace/viewport/widgets.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/widgets.cljs @@ -10,7 +10,7 @@ [app.common.data.macros :as dm] [app.common.geom.point :as gpt] [app.common.geom.shapes :as gsh] - [app.common.pages.helpers :as cph] + [app.common.types.shape-tree :as ctt] [app.common.uuid :as uuid] [app.main.data.workspace :as dw] [app.main.data.workspace.interactions :as dwi] @@ -178,7 +178,7 @@ on-frame-enter (unchecked-get props "on-frame-enter") on-frame-leave (unchecked-get props "on-frame-leave") on-frame-select (unchecked-get props "on-frame-select") - frames (cph/get-frames objects)] + frames (ctt/get-frames objects)] [:g.frame-titles (for [frame frames] diff --git a/frontend/src/app/render.cljs b/frontend/src/app/render.cljs index 872e0f247..8e4f98182 100644 --- a/frontend/src/app/render.cljs +++ b/frontend/src/app/render.cljs @@ -13,6 +13,7 @@ [app.common.uri :as u] [app.config :as cf] [app.main.data.fonts :as df] + [app.main.features :as features] [app.main.render :as render] [app.main.repo :as repo] [app.main.store :as st] @@ -99,22 +100,24 @@ (mf/defc object-svg [{:keys [page-id file-id object-id render-embed?]}] - (let [fetch-state (mf/use-fn - (mf/deps file-id page-id object-id) - (fn [] - (->> (rx/zip - (repo/query! :font-variants {:file-id file-id}) - (repo/query! :page {:file-id file-id - :page-id page-id - :object-id object-id})) - (rx/tap (fn [[fonts]] - (when (seq fonts) - (st/emit! (df/fonts-fetched fonts))))) - (rx/map (comp :objects second)) - (rx/map (fn [objects] - (let [objects (render/adapt-objects-for-shape objects object-id)] - {:objects objects - :object (get objects object-id)})))))) + (let [components-v2 (features/use-feature :components-v2) + fetch-state (mf/use-fn + (mf/deps file-id page-id object-id) + (fn [] + (->> (rx/zip + (repo/query! :font-variants {:file-id file-id}) + (repo/query! :page {:file-id file-id + :page-id page-id + :object-id object-id + :components-v2 components-v2})) + (rx/tap (fn [[fonts]] + (when (seq fonts) + (st/emit! (df/fonts-fetched fonts))))) + (rx/map (comp :objects second)) + (rx/map (fn [objects] + (let [objects (render/adapt-objects-for-shape objects object-id)] + {:objects objects + :object (get objects object-id)})))))) {:keys [objects object]} (use-resource fetch-state)] @@ -124,8 +127,8 @@ (when object (dom/set-page-style! {:size (str/concat - (mth/ceil (:width object)) "px " - (mth/ceil (:height object)) "px")}))) + (mth/ceil (:width object)) "px " + (mth/ceil (:height object)) "px")}))) (when objects [:& render/object-svg @@ -135,17 +138,19 @@ (mf/defc objects-svg [{:keys [page-id file-id object-ids render-embed?]}] - (let [fetch-state (mf/use-fn - (mf/deps file-id page-id) - (fn [] - (->> (rx/zip - (repo/query! :font-variants {:file-id file-id}) - (repo/query! :page {:file-id file-id - :page-id page-id})) - (rx/tap (fn [[fonts]] - (when (seq fonts) - (st/emit! (df/fonts-fetched fonts))))) - (rx/map (comp :objects second))))) + (let [components-v2 (features/use-feature :components-v2) + fetch-state (mf/use-fn + (mf/deps file-id page-id) + (fn [] + (->> (rx/zip + (repo/query! :font-variants {:file-id file-id}) + (repo/query! :page {:file-id file-id + :page-id page-id + :components-v2 components-v2})) + (rx/tap (fn [[fonts]] + (when (seq fonts) + (st/emit! (df/fonts-fetched fonts))))) + (rx/map (comp :objects second))))) objects (use-resource fetch-state)] diff --git a/frontend/src/app/util/geom/snap_points.cljs b/frontend/src/app/util/geom/snap_points.cljs index 1940deaf7..8a7a1cbe6 100644 --- a/frontend/src/app/util/geom/snap_points.cljs +++ b/frontend/src/app/util/geom/snap_points.cljs @@ -8,7 +8,8 @@ (:require [app.common.geom.point :as gpt] [app.common.geom.shapes :as gsh] - [app.common.pages.helpers :as cph])) + [app.common.pages.helpers :as cph] + [app.common.types.shape-tree :as ctst])) (defn selrect-snap-points [{:keys [x y width height] :as selrect}] #{(gpt/point x y) @@ -38,7 +39,7 @@ (cond (and (some? frame) - (not (cph/rotated-frame? frame)) + (not (ctst/rotated-frame? frame)) (not (cph/root-frame? frame))) #{} diff --git a/frontend/src/app/util/names.cljs b/frontend/src/app/util/names.cljs deleted file mode 100644 index 6a2288fcd..000000000 --- a/frontend/src/app/util/names.cljs +++ /dev/null @@ -1,38 +0,0 @@ -;; 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.util.names - (:require - [app.common.data :as d] - [app.common.spec :as us] - [cljs.spec.alpha :as s])) - -(s/def ::set-of-string (s/every string? :kind set?)) - -(defn- extract-numeric-suffix - [basename] - (if-let [[_ p1 p2] (re-find #"(.*)-([0-9]+)$" basename)] - [p1 (+ 1 (d/parse-integer p2))] - [basename 1])) - -(defn retrieve-used-names - [objects] - (into #{} (comp (map :name) (remove nil?)) (vals objects))) - -(defn generate-unique-name - "A unique name generator" - [used basename] - (s/assert ::set-of-string used) - (s/assert ::us/string basename) - (if-not (contains? used basename) - basename - (let [[prefix initial] (extract-numeric-suffix basename)] - (loop [counter initial] - (let [candidate (str prefix "-" counter)] - (if (contains? used candidate) - (recur (inc counter)) - candidate)))))) - diff --git a/frontend/src/app/util/snap_data.cljs b/frontend/src/app/util/snap_data.cljs index 81f60b952..916f1b992 100644 --- a/frontend/src/app/util/snap_data.cljs +++ b/frontend/src/app/util/snap_data.cljs @@ -12,6 +12,7 @@ [app.common.data :as d] [app.common.pages.diff :as diff] [app.common.pages.helpers :as cph] + [app.common.types.shape-tree :as ctst] [app.common.uuid :as uuid] [app.util.geom.grid :as gg] [app.util.geom.snap-points :as snap] @@ -55,7 +56,7 @@ (defn get-grids-snap-points [frame coord] - (if (not (cph/rotated-frame? frame)) + (if (not (ctst/rotated-frame? frame)) [] (let [grid->snap (fn [[grid-type position]] {:type :layout @@ -196,7 +197,7 @@ (defn add-page "Adds page information" [snap-data {:keys [objects options] :as page}] - (let [frames (cph/get-frames objects) + (let [frames (ctst/get-frames objects) shapes (->> (vals (:objects page)) (remove cph/frame-shape?)) guides (vals (:guides options)) diff --git a/frontend/src/app/worker/export.cljs b/frontend/src/app/worker/export.cljs index bd5521110..1427ebd8a 100644 --- a/frontend/src/app/worker/export.cljs +++ b/frontend/src/app/worker/export.cljs @@ -149,8 +149,8 @@ (->> (r/render-components (:data file)) (rx/map #(vector (str (:id file) "/components.svg") %)))) -(defn fetch-file-with-libraries [file-id] - (->> (rx/zip (rp/query :file {:id file-id}) +(defn fetch-file-with-libraries [file-id components-v2] + (->> (rx/zip (rp/query :file {:id file-id :components-v2 components-v2}) (rp/query :file-libraries {:file-id file-id})) (rx/map (fn [[file file-libraries]] @@ -351,7 +351,7 @@ (update file-id dissoc :libraries)))) (defn collect-files - [file-id export-type] + [file-id export-type components-v2] (letfn [(fetch-dependencies [[files pending]] (if (empty? pending) @@ -365,7 +365,7 @@ ;; The file is already in the result (rx/of [files pending]) - (->> (fetch-file-with-libraries next) + (->> (fetch-file-with-libraries next components-v2) (rx/map (fn [file] [(-> files @@ -381,9 +381,9 @@ (rx/map #(process-export file-id export-type %)))))) (defn export-file - [team-id file-id export-type] + [team-id file-id export-type components-v2] - (let [files-stream (->> (collect-files file-id export-type) + (let [files-stream (->> (collect-files file-id export-type components-v2) (rx/share)) manifest-stream @@ -471,12 +471,12 @@ :file-id (:id file)})))))))) (defmethod impl/handler :export-standard-file - [{:keys [team-id files export-type] :as message}] + [{:keys [team-id files export-type components-v2] :as message}] (->> (rx/from files) (rx/mapcat (fn [file] - (->> (export-file team-id (:id file) export-type) + (->> (export-file team-id (:id file) export-type components-v2) (rx/map (fn [value] (if (contains? value :type) diff --git a/frontend/src/app/worker/import.cljs b/frontend/src/app/worker/import.cljs index aff53ab5b..b606ca3db 100644 --- a/frontend/src/app/worker/import.cljs +++ b/frontend/src/app/worker/import.cljs @@ -14,8 +14,8 @@ [app.common.geom.shapes.path :as gpa] [app.common.logging :as log] [app.common.media :as cm] - [app.common.pages :as cp] [app.common.text :as ct] + [app.common.types.file :as ctf] [app.common.uuid :as uuid] [app.main.repo :as rp] [app.util.http :as http] @@ -133,7 +133,7 @@ :name (:name context) :is-shared (:shared context) :project-id (:project-id context) - :data (-> cp/empty-file-data (assoc :id file-id))}))) + :data (-> ctf/empty-file-data (assoc :id file-id))}))) (defn link-file-libraries "Create a new file on the back-end" diff --git a/frontend/src/app/worker/thumbnails.cljs b/frontend/src/app/worker/thumbnails.cljs index cc28169be..1dc58811c 100644 --- a/frontend/src/app/worker/thumbnails.cljs +++ b/frontend/src/app/worker/thumbnails.cljs @@ -48,11 +48,12 @@ (= :request-body-too-large code))) (defn- request-data-for-thumbnail - [file-id revn] + [file-id revn components-v2] (let [path "api/rpc/query/file-data-for-thumbnail" params {:file-id file-id :revn revn - :strip-frames-with-thumbnails true} + :strip-frames-with-thumbnails true + :components-v2 components-v2} request {:method :get :uri (u/join (cfg/get-public-uri) path) :credentials "include" @@ -107,18 +108,18 @@ (rx/map (constantly params))))) (defmethod impl/handler :thumbnails/generate - [{:keys [file-id revn] :as message}] + [{:keys [file-id revn components-v2] :as message}] (letfn [(on-result [{:keys [data props]}] {:data data :fonts (:fonts props)}) (on-cache-miss [_] - (->> (request-data-for-thumbnail file-id revn) + (->> (request-data-for-thumbnail file-id revn components-v2) (rx/map render-thumbnail) (rx/mapcat persist-thumbnail)))] (if (debug? :disable-thumbnail-cache) - (->> (request-data-for-thumbnail file-id revn) + (->> (request-data-for-thumbnail file-id revn components-v2) (rx/map render-thumbnail)) (->> (request-thumbnail file-id revn) (rx/catch not-found? on-cache-miss) diff --git a/frontend/src/debug.cljs b/frontend/src/debug.cljs index bef2d105d..077993bc3 100644 --- a/frontend/src/debug.cljs +++ b/frontend/src/debug.cljs @@ -8,8 +8,8 @@ (:require [app.common.data :as d] [app.common.logging :as l] - [app.common.pages.helpers :as cph] [app.common.transit :as t] + [app.common.types.file :as ctf] [app.common.uuid :as uuid] [app.main.data.dashboard.shortcuts] [app.main.data.viewer.shortcuts] @@ -211,73 +211,9 @@ ([state show-ids] (dump-tree' state show-ids false)) ([state show-ids show-touched] (let [page-id (get state :current-page-id) - objects (get-in state [:workspace-data :pages-index page-id :objects]) - components (get-in state [:workspace-data :components]) - libraries (get state :workspace-libraries) - root (d/seek #(nil? (:parent-id %)) (vals objects))] - - (letfn [(show-shape [shape-id level objects] - (let [shape (get objects shape-id)] - (println (str/pad (str (str/repeat " " level) - (:name shape) - (when (seq (:touched shape)) "*") - (when show-ids (str/format " <%s>" (:id shape)))) - {:length 20 - :type :right}) - (show-component shape objects)) - (when show-touched - (when (seq (:touched shape)) - (println (str (str/repeat " " level) - " " - (str (:touched shape))))) - (when (:remote-synced? shape) - (println (str (str/repeat " " level) - " (remote-synced)")))) - (when (:shapes shape) - (dorun (for [shape-id (:shapes shape)] - (show-shape shape-id (inc level) objects)))))) - - (show-component [shape objects] - (if (nil? (:shape-ref shape)) - "" - (let [root-shape (cph/get-component-shape objects shape) - component-id (when root-shape (:component-id root-shape)) - component-file-id (when root-shape (:component-file root-shape)) - component-file (when component-file-id (get libraries component-file-id nil)) - component (when component-id - (if component-file - (get-in component-file [:data :components component-id]) - (get components component-id))) - component-shape (when (and component (:shape-ref shape)) - (get-in component [:objects (:shape-ref shape)]))] - (str/format " %s--> %s%s%s" - (cond (:component-root? shape) "#" - (:component-id shape) "@" - :else "-") - (when component-file (str/format "<%s> " (:name component-file))) - (or (:name component-shape) "?") - (if (or (:component-root? shape) - (nil? (:component-id shape)) - true) - "" - (let [component-id (:component-id shape) - component-file-id (:component-file shape) - component-file (when component-file-id (get libraries component-file-id nil)) - component (if component-file - (get-in component-file [:data :components component-id]) - (get components component-id))] - (str/format " (%s%s)" - (when component-file (str/format "<%s> " (:name component-file))) - (:name component))))))))] - - (println "[Page]") - (show-shape (:id root) 0 objects) - - (dorun (for [component (vals components)] - (do - (println) - (println (str/format "[%s]" (:name component))) - (show-shape (:id component) 0 (:objects component))))))))) + file-data (get state :workspace-data) + libraries (get state :workspace-libraries)] + (ctf/dump-tree file-data page-id libraries show-ids show-touched)))) (defn ^:export dump-tree ([] (dump-tree' @st/state)) diff --git a/frontend/src/features.cljs b/frontend/src/features.cljs index 353504a1e..f0e9af722 100644 --- a/frontend/src/features.cljs +++ b/frontend/src/features.cljs @@ -7,8 +7,11 @@ ;; This namespace is only to export the functions for toggle features (ns features (:require - [app.main.ui.features :as features])) + [app.main.features :as features])) (defn ^:export autolayout [] (features/toggle-feature! :auto-layout)) +(defn ^:export components-v2 [] + (features/toggle-feature! :components-v2)) + diff --git a/frontend/test/app/components_basic_test.cljs b/frontend/test/app/components_basic_test.cljs index b392e7100..52958aee5 100644 --- a/frontend/test/app/components_basic_test.cljs +++ b/frontend/test/app/components_basic_test.cljs @@ -3,6 +3,7 @@ [app.common.data :as d] [app.common.geom.point :as gpt] [app.common.pages.helpers :as cph] + [app.common.types.container :as ctn] [app.main.data.workspace :as dw] [app.main.data.workspace.groups :as dwg] [app.main.data.workspace.libraries :as dwl] @@ -340,7 +341,7 @@ (ptk/emit! store (dwl/delete-component {:id component-id}) - (dwl/sync-file (:id file) (:id file)) + (dwl/sync-file (:id file) (:id file) :components component-id) :the/end)))) (t/deftest test-instantiate-component @@ -520,7 +521,7 @@ ; (let [page (thp/current-page new-state) shape1 (thp/get-shape new-state :shape1) - parent1 (cph/get-shape page (:parent-id shape1)) + parent1 (ctn/get-shape page (:parent-id shape1)) [[group shape1 shape2] [c-group c-shape1 c-shape2] diff --git a/frontend/test/app/components_sync_test.cljs b/frontend/test/app/components_sync_test.cljs index b3d754a37..05bf78ea2 100644 --- a/frontend/test/app/components_sync_test.cljs +++ b/frontend/test/app/components_sync_test.cljs @@ -4,6 +4,7 @@ [app.common.data :as d] [app.common.geom.point :as gpt] [app.common.pages.helpers :as cph] + [app.common.types.container :as ctn] [app.main.data.workspace :as dw] [app.main.data.workspace.changes :as dch] [app.main.data.workspace.shapes :as dwsh] @@ -1352,7 +1353,7 @@ instance1 (thp/get-shape state :instance1) instance2 (thp/get-shape state :instance2) - shape2 (cph/get-shape (wsh/lookup-page state) + shape2 (ctn/get-shape (wsh/lookup-page state) (first (:shapes instance2))) update-fn1 (fn [shape] diff --git a/frontend/test/app/test_helpers/libraries.cljs b/frontend/test/app/test_helpers/libraries.cljs index f3644d8e3..b2754977b 100644 --- a/frontend/test/app/test_helpers/libraries.cljs +++ b/frontend/test/app/test_helpers/libraries.cljs @@ -2,14 +2,9 @@ (:require [cljs.test :as t :include-macros true] [cljs.pprint :refer [pprint]] - [beicon.core :as rx] - [potok.core :as ptk] - [app.common.uuid :as uuid] - [app.common.geom.point :as gpt] - [app.common.geom.shapes :as gsh] [app.common.pages.helpers :as cph] - [app.main.data.workspace :as dw] - [app.main.data.workspace.libraries-helpers :as dwlh] + [app.common.types.component :as ctk] + [app.common.types.container :as ctn] [app.main.data.workspace.state-helpers :as wsh] [app.test-helpers.pages :as thp])) @@ -59,7 +54,7 @@ verify that they are a well constructed instance tree." [state root-inst-id] (let [page (thp/current-page state) - root-inst (cph/get-shape page root-inst-id) + root-inst (ctn/get-shape page root-inst-id) shapes-inst (cph/get-children-with-self (:objects page) root-inst-id)] (is-instance-root (first shapes-inst)) @@ -72,7 +67,7 @@ verify that they are not a component instance." [state root-inst-id] (let [page (thp/current-page state) - root-inst (cph/get-shape page root-inst-id) + root-inst (ctn/get-shape page root-inst-id) shapes-inst (cph/get-children-with-self (:objects page) root-inst-id)] (run! is-noninstance shapes-inst) @@ -84,7 +79,7 @@ the main component and all its shapes." [state root-inst-id] (let [page (thp/current-page state) - root-inst (cph/get-shape page root-inst-id) + root-inst (ctn/get-shape page root-inst-id) libs (wsh/get-libraries state) component (cph/get-component libs (:component-id root-inst)) @@ -102,7 +97,7 @@ (cph/get-component libs (:component-id component-shape)) main-shape - (cph/get-shape component (:shape-ref shape))] + (ctn/get-shape component (:shape-ref shape))] (t/is (some? main-shape))))] @@ -122,7 +117,7 @@ corresponding component shape missing." [state root-inst-id] (let [page (thp/current-page state) - root-inst (cph/get-shape page root-inst-id) + root-inst (ctn/get-shape page root-inst-id) libs (wsh/get-libraries state) component (cph/get-component libs (:component-id root-inst)) @@ -140,7 +135,7 @@ (cph/get-component libs (:component-id component-shape)) main-shape - (cph/get-shape component (:shape-ref shape))] + (ctn/get-shape component (:shape-ref shape))] (t/is (some? main-shape))))] @@ -155,7 +150,7 @@ (let [page (thp/current-page state) libs (wsh/get-libraries state) component (cph/get-component libs component-id) - root-main (cph/get-component-root component) + root-main (ctk/get-component-root component) shapes-main (cph/get-children-with-self (:objects component) (:id root-main))] ;; Validate that the component tree is well constructed diff --git a/frontend/test/app/test_helpers/pages.cljs b/frontend/test/app/test_helpers/pages.cljs index 6156a02ce..e66974d01 100644 --- a/frontend/test/app/test_helpers/pages.cljs +++ b/frontend/test/app/test_helpers/pages.cljs @@ -9,6 +9,7 @@ [app.common.geom.shapes :as gsh] [app.common.pages :as cp] [app.common.pages.helpers :as cph] + [app.common.types.shape :as cts] [app.main.data.workspace :as dw] [app.main.data.workspace.groups :as dwg] [app.main.data.workspace.layout :as layout] @@ -69,9 +70,7 @@ ([state label type props] (let [page (current-page state) frame (cph/get-frame (:objects page)) - shape (-> (cp/make-minimal-shape type) - (cp/setup-shape {:x 0 :y 0 :width 1 :height 1}) - (merge props))] + shape (cts/make-shape type {:x 0 :y 0 :width 1 :height 1} props)] (swap! idmap assoc label (:id shape)) (update state :workspace-data cp/process-changes @@ -106,7 +105,8 @@ shapes (:objects page) (:id page) - current-file-id)] + current-file-id + true)] (swap! idmap assoc instance-label (:id group) component-label (:id component-root)) diff --git a/frontend/test/app/util/snap_data_test.cljs b/frontend/test/app/util/snap_data_test.cljs index 6982d4f6f..9f18e5a1c 100644 --- a/frontend/test/app/util/snap_data_test.cljs +++ b/frontend/test/app/util/snap_data_test.cljs @@ -9,7 +9,6 @@ [app.common.uuid :as uuid] [cljs.test :as t :include-macros true] [cljs.pprint :refer [pprint]] - [app.common.pages.init :as init] [app.common.file-builder :as fb] [app.util.snap-data :as sd])) diff --git a/frontend/translations/en.po b/frontend/translations/en.po index 3fead2e7f..8191aa81e 100644 --- a/frontend/translations/en.po +++ b/frontend/translations/en.po @@ -539,6 +539,10 @@ msgstr "Want to remove your account?" msgid "dashboard.remove-shared" msgstr "Remove as Shared Library" +#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +msgid "dashboard.unpublish-shared" +msgstr "Unpublish Library" + #: src/app/main/ui/settings/profile.cljs msgid "dashboard.save-settings" msgstr "Save settings" @@ -639,6 +643,14 @@ msgstr "Your name" msgid "dashboard.your-penpot" msgstr "Your Penpot" +#: src/app/main/ui/alert.cljs +msgid "ds.alert-ok" +msgstr "Ok" + +#: src/app/main/ui/alert.cljs +msgid "ds.alert-title" +msgstr "Attention" + #: src/app/main/ui/confirm.cljs msgid "ds.component-subtitle" msgstr "Components to update:" @@ -718,6 +730,10 @@ msgstr "LDAP authentication is disabled." msgid "errors.media-format-unsupported" msgstr "The image format is not supported (must be svg, jpg or png)." +#: src/app/main/data/workspace/persistence.cljs +msgid "errors.components-v2" +msgstr "This file has already used with Components V2 enabled." + #: src/app/main/data/workspace/persistence.cljs msgid "errors.media-too-large" msgstr "The image is too large to be inserted (must be under 5mb)." @@ -1763,6 +1779,49 @@ msgstr "" msgid "modals.remove-shared-confirm.message" msgstr "Remove “%s” as Shared Library" +#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +msgid "modals.unpublish-shared-confirm.title" +msgstr "Unpublish library" + +#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +msgid "modals.unpublish-shared-confirm.message" +msgstr "Are you sure you want to unpublish this library?" + +#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +msgid "modals.unpublish-shared-confirm.scd-message" +msgid_plural "modals.unpublish-shared-confirm.scd-message" +msgstr[0] "It's in use in this file:" +msgstr[1] "It's in use in these files:" + +#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +msgid "modals.unpublish-shared-confirm.hint" +msgid_plural "modals.unpublish-shared-confirm.hint" +msgstr[0] "If you unpublish it, the assets in it became a library of this file." +msgstr[1] "If you unpublish it, the assets in it became a library of these files." + +#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +msgid "modals.unpublish-shared-confirm.accept" +msgstr "Unpublish" + +#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +msgid "modals.delete-shared-confirm.title" +msgstr "Deleting file" + +#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +msgid "modals.delete-shared-confirm.message" +msgstr "Are you sure you want to delete this file?" + +#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +msgid "modals.delete-shared-confirm.scd-message" +msgid_plural "modals.delete-shared-confirm.scd-message" +msgstr[0] "This file has libraries that are being used in this file:" +msgstr[1] "This file has libraries that are being used in these files:" + +#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +msgid "modals.delete-shared-confirm.accept" +msgstr "Delete file" + + #: src/app/main/ui/workspace/nudge.cljs msgid "modals.small-nudge" msgstr "Small nudge" @@ -1795,6 +1854,10 @@ msgstr "" msgid "modals.update-remote-component.message" msgstr "Update a component in a shared library" +#: src/app/main/ui/delete_shared.cljs +msgid "modals.delete-shared.title" +msgstr "Deleting file" + #: src/app/main/ui/dashboard/team.cljs msgid "notifications.invitation-email-sent" msgstr "Invitation sent successfully" diff --git a/frontend/translations/es.po b/frontend/translations/es.po index 68698a8ad..38ca85826 100644 --- a/frontend/translations/es.po +++ b/frontend/translations/es.po @@ -551,6 +551,10 @@ msgstr "¿Quieres borrar tu cuenta?" msgid "dashboard.remove-shared" msgstr "Eliminar como Biblioteca Compartida" +#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +msgid "dashboard.unpublish-shared" +msgstr "Despublicar Biblioteca" + #: src/app/main/ui/settings/profile.cljs msgid "dashboard.save-settings" msgstr "Guardar opciones" @@ -656,6 +660,14 @@ msgstr "Tu nombre" msgid "dashboard.your-penpot" msgstr "Tu Penpot" +#: src/app/main/ui/alert.cljs +msgid "ds.alert-ok" +msgstr "Ok" + +#: src/app/main/ui/alert.cljs +msgid "ds.alert-title" +msgstr "Atención" + #: src/app/main/ui/confirm.cljs msgid "ds.component-subtitle" msgstr "Componentes a actualizar:" @@ -736,6 +748,10 @@ msgstr "La autheticacion via LDAP esta deshabilitada." msgid "errors.media-format-unsupported" msgstr "No se reconoce el formato de imagen (debe ser svg, jpg o png)." +#: src/app/main/data/workspace/persistence.cljs +msgid "errors.components-v2" +msgstr "Este fichero ya se ha usado con los Componentes V2 activados." + #: src/app/main/data/workspace/persistence.cljs msgid "errors.media-too-large" msgstr "La imagen es demasiado grande (debe tener menos de 5mb)." @@ -1837,6 +1853,48 @@ msgstr "" msgid "modals.remove-shared-confirm.message" msgstr "Añadir “%s” como Biblioteca Compartida" +#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +msgid "modals.unpublish-shared-confirm.title" +msgstr "Despublicar biblioteca" + +#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +msgid "modals.unpublish-shared-confirm.message" +msgstr "¿Seguro que quieres despublicar esta biblioteca?" + +#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +msgid "modals.unpublish-shared-confirm.scd-message" +msgid_plural "modals.unpublish-shared-confirm.scd-message" +msgstr[0] "Está siendo usada en este archivo:" +msgstr[1] "Está siendo usada en estos archivos:" + +#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +msgid "modals.unpublish-shared-confirm.hint" +msgid_plural "modals.unpublish-shared-confirm.hint" +msgstr[0] "Si la despublicas, los elementos pasarán a formar parte de la biblioteca del archivo." +msgstr[1] "Si la despublicas, los elementos pasarán a formar parte de la biblioteca de los archivos." + +#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +msgid "modals.unpublish-shared-confirm.accept" +msgstr "Despublicar" + +#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +msgid "modals.delete-shared-confirm.title" +msgstr "Borrar archivo" + +#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +msgid "modals.delete-shared-confirm.message" +msgstr "¿Seguro que quieres borrar este archivo?" + +#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +msgid "modals.delete-shared-confirm.scd-message" +msgid_plural "modals.delete-shared-confirm.scd-message" +msgstr[0] "El archivo que quieres borrar tiene una librería que se está usando en este archivo:" +msgstr[1] "El archivo que quieres borrar tiene una librería que se está usando en estos archivos:" + +#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +msgid "modals.delete-shared-confirm.accept" +msgstr "Borrar archivo" + #: src/app/main/ui/workspace/nudge.cljs msgid "modals.small-nudge" msgstr "Mínimo" @@ -1875,6 +1933,10 @@ msgstr "" msgid "modals.update-remote-component.message" msgstr "Actualizar un componente en librería" +#: src/app/main/ui/delete_shared.cljs +msgid "modals.delete-shared.title" +msgstr "Borrar archivo" + #: src/app/main/ui/dashboard/team.cljs msgid "notifications.invitation-email-sent" msgstr "Invitación enviada con éxito"