0
Fork 0
mirror of https://github.com/penpot/penpot.git synced 2025-03-18 18:51:29 -05:00

Merge pull request #2108 from penpot/hiru-main-instance

Components v2 (first PR)
This commit is contained in:
Andrey Antukh 2022-08-01 13:25:06 +02:00 committed by GitHub
commit 18970cb233
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
101 changed files with 3212 additions and 1262 deletions

View file

@ -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)

View file

@ -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)))

View file

@ -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))]

View file

@ -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 #{}

View file

@ -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))

View file

@ -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)

View file

@ -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}))))

View file

@ -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))

View file

@ -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))]

View file

@ -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"

View file

@ -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)

View file

@ -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)

View file

@ -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))}))))

View file

@ -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]

View file

@ -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))))

View file

@ -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))))

View file

@ -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))))))

View file

@ -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)

View file

@ -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 $))))

View file

@ -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))

View file

@ -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)))

View file

@ -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))

View file

@ -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]))

View file

@ -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)))))))))

View file

@ -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

View file

@ -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))

View file

@ -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)))

View file

@ -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}))))

View file

@ -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))

View file

@ -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)))))

View file

@ -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)

View file

@ -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"

View file

@ -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]))

View file

@ -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)))))

View file

@ -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))))

View file

@ -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

View file

@ -0,0 +1,3 @@
<svg width="500" height="500" viewBox="2115 2827.9 500 500" xmlns="http://www.w3.org/2000/svg">
<path d="m2615 3077.69-140.71 250.21h-218.22L2115 3077.69l141.07-249.79h218.22zm-337.86 207.73h176.07l118.93-207.3-118.93-207.74h-176.07l-118.93 208.16z"/>
</svg>

After

Width:  |  Height:  |  Size: 261 B

View file

@ -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 {

View file

@ -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)))

View file

@ -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]

View file

@ -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)

View file

@ -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

View file

@ -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)

View file

@ -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)

View file

@ -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))

View file

@ -108,4 +108,3 @@
:undo-changes []
:origin it
:save-undo? false})))))))))))

View file

@ -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

View file

@ -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})

View file

@ -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)

View file

@ -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))))))

View file

@ -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

View file

@ -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)

View file

@ -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

View file

@ -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

View file

@ -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))))))

View file

@ -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

View file

@ -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

View file

@ -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))))))

View file

@ -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

View file

@ -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

View file

@ -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)))))

View file

@ -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))

View file

@ -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

View file

@ -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}]]]]]))

View file

@ -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))

View file

@ -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))

View file

@ -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]

View file

@ -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"]

View file

@ -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}]]]))

View file

@ -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?

View file

@ -42,5 +42,6 @@
[:h1 (tr "dashboard.libraries-title")]]]
[:section.dashboard-container
[:& grid {:files files
:project default-project}]]]))
:project default-project
:origin :libraries}]]]))

View file

@ -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))

View file

@ -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}]]]]]))

View file

@ -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))

View file

@ -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])]]]]]]))

View file

@ -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)

View file

@ -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

View file

@ -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}

View file

@ -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)

View file

@ -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]]

View file

@ -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

View file

@ -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)]

View file

@ -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?)

View file

@ -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

View file

@ -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]

View file

@ -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)]

View file

@ -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)))
#{}

View file

@ -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))))))

View file

@ -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))

View file

@ -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)

View file

@ -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"

View file

@ -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)

View file

@ -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))

View file

@ -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))

View file

@ -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]

View file

@ -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]

View file

@ -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

View file

@ -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))

View file

@ -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]))

View file

@ -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"

Some files were not shown because too many files have changed in this diff Show more