Merge pull request #3189 from penpot/hiru-sync-notifications

 Notify library updates when really needed
This commit is contained in:
Pablo Alba 2023-05-17 15:35:06 +02:00 committed by GitHub
commit 44a3f651c2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
32 changed files with 373 additions and 195 deletions

View file

@ -253,7 +253,6 @@
:code :feature-mismatch :code :feature-mismatch
:feature "components/v2" :feature "components/v2"
:hint "file has 'components/v2' feature enabled but frontend didn't specifies it")) :hint "file has 'components/v2' feature enabled but frontend didn't specifies it"))
(cond-> file (cond-> file
(and (contains? client-features "components/v2") (and (contains? client-features "components/v2")
(not (contains? features "components/v2"))) (not (contains? features "components/v2")))
@ -263,7 +262,6 @@
(not (contains? client-features "storage/pointer-map"))) (not (contains? client-features "storage/pointer-map")))
(process-pointers deref))) (process-pointers deref)))
;; --- COMMAND QUERY: get-file (by id) ;; --- COMMAND QUERY: get-file (by id)
(defn get-file (defn get-file

View file

@ -78,8 +78,7 @@
(defn- library-change? (defn- library-change?
[{:keys [type] :as change}] [{:keys [type] :as change}]
(or (contains? library-change-types type) (or (contains? library-change-types type)
(and (contains? file-change-types type) (contains? file-change-types type)))
(some? (:component-id change)))))
(def ^:private sql:get-file (def ^:private sql:get-file
"SELECT f.*, p.team_id "SELECT f.*, p.team_id

View file

@ -6,7 +6,9 @@
(ns app.util.time (ns app.util.time
(:require (:require
[app.common.data.macros :as dm]
[app.common.exceptions :as ex] [app.common.exceptions :as ex]
[app.common.time :as common-time]
[clojure.spec.alpha :as s] [clojure.spec.alpha :as s]
[cuerdas.core :as str] [cuerdas.core :as str]
[fipp.ednize :as fez]) [fipp.ednize :as fez])
@ -186,9 +188,7 @@
:else :else
(throw (UnsupportedOperationException. "unsupported type"))))) (throw (UnsupportedOperationException. "unsupported type")))))
(defn now (dm/export common-time/now)
[]
(Instant/now))
(defn in-future (defn in-future
[v] [v]

View file

@ -4,7 +4,7 @@
"main": "index.js", "main": "index.js",
"license": "MPL-2.0", "license": "MPL-2.0",
"dependencies": { "dependencies": {
"luxon": "^3.1.1" "luxon": "^3.3.0"
}, },
"scripts": { "scripts": {
"compile-and-watch-test": "clojure -M:dev:shadow-cljs watch test", "compile-and-watch-test": "clojure -M:dev:shadow-cljs watch test",

View file

@ -16,6 +16,7 @@
[app.common.pages.helpers :as cph] [app.common.pages.helpers :as cph]
[app.common.spec :as us] [app.common.spec :as us]
[app.common.pages.changes-spec :as pcs] [app.common.pages.changes-spec :as pcs]
[app.common.types.component :as ctk]
[app.common.types.components-list :as ctkl] [app.common.types.components-list :as ctkl]
[app.common.types.container :as ctn] [app.common.types.container :as ctn]
[app.common.types.colors-list :as ctcl] [app.common.types.colors-list :as ctcl]
@ -53,7 +54,7 @@
(run! validate-shape!))))) (run! validate-shape!)))))
(defmulti process-change (fn [_ change] (:type change))) (defmulti process-change (fn [_ change] (:type change)))
(defmulti process-operation (fn [_ op] (:type op))) (defmulti process-operation (fn [_ _ op] (:type op)))
(defn process-changes (defn process-changes
([data items] ([data items]
@ -91,14 +92,34 @@
(defmethod process-change :mod-obj (defmethod process-change :mod-obj
[data {:keys [id page-id component-id operations]}] [data {:keys [id page-id component-id operations]}]
(let [update-fn (fn [objects] (let [objects (if page-id
(-> data :pages-index (get page-id) :objects)
(-> data :components (get component-id) :objects))
modified-component-ids (atom #{})
on-touched (fn [shape]
;; When a shape is modified, if it belongs to a main component instance,
;; the component needs to be marked as modified.
(let [component-root (ctn/get-component-shape objects shape {:allow-main? true})]
(when (ctk/main-instance? component-root)
(swap! modified-component-ids conj (:component-id component-root)))))
update-fn (fn [objects]
(if-let [obj (get objects id)] (if-let [obj (get objects id)]
(let [result (reduce process-operation obj operations)] (let [result (reduce (partial process-operation on-touched) obj operations)]
(assoc objects id result)) (assoc objects id result))
objects))] objects))
(if page-id
(d/update-in-when data [:pages-index page-id :objects] update-fn) modify-components (fn [data]
(d/update-in-when data [:components component-id :objects] update-fn)))) (reduce ctkl/set-component-modified
data @modified-component-ids))]
(as-> data $
(if page-id
(d/update-in-when $ [:pages-index page-id :objects] update-fn)
(d/update-in-when $ [:components component-id :objects] update-fn))
(modify-components $))))
(defmethod process-change :del-obj (defmethod process-change :del-obj
[data {:keys [page-id component-id id ignore-touched]}] [data {:keys [page-id component-id id ignore-touched]}]
@ -223,8 +244,6 @@
(not= :frame (:type obj)) (not= :frame (:type obj))
(as-> $$ (reduce (partial assign-frame-id frame-id) $$ (:shapes obj)))))) (as-> $$ (reduce (partial assign-frame-id frame-id) $$ (:shapes obj))))))
(move-objects [objects] (move-objects [objects]
(let [valid? (every? (partial is-valid-move? objects) shapes) (let [valid? (every? (partial is-valid-move? objects) shapes)
parent (get objects parent-id) parent (get objects parent-id)
@ -284,7 +303,7 @@
(defmethod process-change :mod-color (defmethod process-change :mod-color
[data {:keys [color]}] [data {:keys [color]}]
(d/assoc-in-when data [:colors (:id color)] color)) (ctcl/set-color data color))
(defmethod process-change :del-color (defmethod process-change :del-color
[data {:keys [id]}] [data {:keys [id]}]
@ -343,7 +362,7 @@
(defmethod process-change :mod-typography (defmethod process-change :mod-typography
[data {:keys [typography]}] [data {:keys [typography]}]
(d/update-in-when data [:typographies (:id typography)] merge typography)) (ctyl/update-typography data (:id typography) merge typography))
(defmethod process-change :del-typography (defmethod process-change :del-typography
[data {:keys [id]}] [data {:keys [id]}]
@ -352,7 +371,7 @@
;; === Operations ;; === Operations
(defmethod process-operation :set (defmethod process-operation :set
[shape op] [on-touched shape op]
(let [attr (:attr op) (let [attr (:attr op)
group (get component-sync-attrs attr) group (get component-sync-attrs attr)
val (:val op) val (:val op)
@ -367,7 +386,7 @@
;; after the check added in data/workspace/modifiers/check-delta ;; after the check added in data/workspace/modifiers/check-delta
;; function. Better check it and test toroughly when activating ;; function. Better check it and test toroughly when activating
;; components-v2 mode. ;; components-v2 mode.
shape-ref (:shape-ref shape) in-copy? (ctk/in-component-copy? shape)
root-name? (and (= group :name-group) root-name? (and (= group :name-group)
(:component-root? shape)) (:component-root? shape))
@ -379,17 +398,23 @@
(gsh/close-attrs? attr val shape-val 1) (gsh/close-attrs? attr val shape-val 1)
(gsh/close-attrs? attr val shape-val))] (gsh/close-attrs? attr val shape-val))]
(when (and group (not ignore) (not equal?)
(not root-name?)
(not (and ignore-geometry is-geometry?)))
;; Notify touched even if it's not copy, because it may be a main instance
(on-touched shape))
(cond-> shape (cond-> shape
;; Depending on the origin of the attribute change, we need or not to ;; Depending on the origin of the attribute change, we need or not to
;; set the "touched" flag for the group the attribute belongs to. ;; set the "touched" flag for the group the attribute belongs to.
;; In some cases we need to ignore touched only if the attribute is ;; In some cases we need to ignore touched only if the attribute is
;; geometric (position, width or transformation). ;; geometric (position, width or transformation).
(and shape-ref group (not ignore) (not equal?) (and in-copy? group (not ignore) (not equal?)
(not root-name?) (not root-name?)
(not (and ignore-geometry is-geometry?))) (not (and ignore-geometry is-geometry?)))
(-> (->
(update :touched cph/set-touched-group group) (update :touched cph/set-touched-group group)
(dissoc :remote-synced?)) (dissoc :remote-synced?))
(nil? val) (nil? val)
(dissoc attr) (dissoc attr)
@ -398,23 +423,23 @@
(assoc attr val)))) (assoc attr val))))
(defmethod process-operation :set-touched (defmethod process-operation :set-touched
[shape op] [_ shape op]
(let [touched (:touched op) (let [touched (:touched op)
shape-ref (:shape-ref shape)] in-copy? (ctk/in-component-copy? shape)]
(if (or (nil? shape-ref) (nil? touched) (empty? touched)) (if (or (not in-copy?) (nil? touched) (empty? touched))
(dissoc shape :touched) (dissoc shape :touched)
(assoc shape :touched touched)))) (assoc shape :touched touched))))
(defmethod process-operation :set-remote-synced (defmethod process-operation :set-remote-synced
[shape op] [_ shape op]
(let [remote-synced? (:remote-synced? op) (let [remote-synced? (:remote-synced? op)
shape-ref (:shape-ref shape)] in-copy? (ctk/in-component-copy? shape)]
(if (or (nil? shape-ref) (not remote-synced?)) (if (or (not in-copy?) (not remote-synced?))
(dissoc shape :remote-synced?) (dissoc shape :remote-synced?)
(assoc shape :remote-synced? true)))) (assoc shape :remote-synced? true))))
(defmethod process-operation :default (defmethod process-operation :default
[_ op] [_ _ op]
(ex/raise :type :not-implemented (ex/raise :type :not-implemented
:code :operation-not-implemented :code :operation-not-implemented
:context {:type (:type op)})) :context {:type (:type op)}))

View file

@ -272,17 +272,6 @@
[shape group] [shape group]
((or (:touched shape) #{}) group)) ((or (:touched shape) #{}) group))
(defn get-root-shape
"Get the root shape linked to a component for this shape, if any."
[objects shape]
(cond
(some? (:component-root? shape))
shape
(some? (:shape-ref shape))
(recur objects (get objects (:parent-id shape)))))
(defn make-container (defn make-container
[page-or-component type] [page-or-component type]
(assoc page-or-component :type type)) (assoc page-or-component :type type))

View file

@ -235,7 +235,7 @@
;; --- SPECS WITHOUT CONFORMER ;; --- SPECS WITHOUT CONFORMER
(s/def ::inst inst?) (s/def ::inst inst?) ;; A clojure instant (date and time)
(s/def ::string (s/def ::string
(s/with-gen string? (s/with-gen string?

View file

@ -0,0 +1,27 @@
;; 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) KALEIDOS INC
;; Here we put the time functions that are common between frontend and backend.
;; In the future we may create an unified API for both.
(ns app.common.time
#?(:cljs
(:require
["luxon" :as lxn])
:clj
(:import
java.time.Instant)))
#?(:cljs
(def DateTime lxn/DateTime))
#?(:cljs
(def Duration lxn/Duration))
(defn now
[]
#?(:clj (Instant/now)
:cljs (.local ^js DateTime)))

View file

@ -58,6 +58,7 @@
(s/def ::color-generic/gradient (s/nilable ::gradient)) (s/def ::color-generic/gradient (s/nilable ::gradient))
(s/def ::color-generic/ref-id uuid?) (s/def ::color-generic/ref-id uuid?)
(s/def ::color-generic/ref-file uuid?) (s/def ::color-generic/ref-file uuid?)
(s/def ::color-generic/modified-at ::us/inst)
(s/def ::shape-color (s/def ::shape-color
(s/keys :req-un [:us/color (s/keys :req-un [:us/color
@ -73,7 +74,8 @@
::color-generic/value ::color-generic/value
::color-generic/color ::color-generic/color
::color-generic/opacity ::color-generic/opacity
::color-generic/gradient])) ::color-generic/gradient
::color-generic/modified-at]))
(s/def ::recent-color (s/def ::recent-color
(s/and (s/and

View file

@ -4,25 +4,51 @@
;; ;;
;; Copyright (c) KALEIDOS INC ;; Copyright (c) KALEIDOS INC
(ns app.common.types.colors-list) (ns app.common.types.colors-list
(:require
[app.common.data :as d]
[app.common.time :as dt]
[app.common.types.color :as ctc]))
(defn colors-seq (defn colors-seq
[file-data] [file-data]
(vals (:colors file-data))) (vals (:colors file-data)))
(defn- touch
[color]
(assoc color :modified-at (dt/now)))
(defn add-color (defn add-color
[file-data color] [file-data color]
(update file-data :colors assoc (:id color) color)) (update file-data :colors assoc (:id color) (touch color)))
(defn get-color (defn get-color
[file-data color-id] [file-data color-id]
(get-in file-data [:colors color-id])) (get-in file-data [:colors color-id]))
(defn get-ref-color
[library-data color]
(when (= (:ref-file color) (:id library-data))
(get-color library-data (:ref-id color))))
(defn set-color
[file-data color]
(d/assoc-in-when file-data [:colors (:id color)] (touch color)))
(defn update-color (defn update-color
[file-data color-id f] [file-data color-id f & args]
(update-in file-data [:colors color-id] f)) (d/update-in-when file-data [:colors color-id] #(-> (apply f % args)
(touch))))
(defn delete-color (defn delete-color
[file-data color-id] [file-data color-id]
(update file-data :colors dissoc color-id)) (update file-data :colors dissoc color-id))
(defn used-colors-changed-since
"Find all usages of any color in the library by the given shape, of colors
that have ben modified after the date."
[shape library since-date]
(->> (ctc/get-all-colors shape)
(keep #(get-ref-color (:data library) %))
(remove #(< (:modified-at %) since-date)) ;; Note that :modified-at may be nil
(map #(vector (:id shape) (:id %) :color))))

View file

@ -7,6 +7,12 @@
(ns app.common.types.component) (ns app.common.types.component)
(defn instance-root? (defn instance-root?
"Check if this shape is the head of a top instance."
[shape]
(some? (:component-root? shape)))
(defn instance-head?
"Check if this shape is the head of a top instance or a subinstance."
[shape] [shape]
(some? (:component-id shape))) (some? (:component-id shape)))
@ -46,12 +52,12 @@
(and (some? (:component-id shape)) (and (some? (:component-id shape))
(= (:component-file shape) library-id))) (= (:component-file shape) library-id)))
(defn in-component-instance? (defn in-component-copy?
"Check if the shape is inside a component non-main instance." "Check if the shape is inside a component non-main instance."
[shape] [shape]
(some? (:shape-ref shape))) (some? (:shape-ref shape)))
(defn in-component-instance-not-root? (defn in-component-copy-not-root?
"Check if the shape is inside a component non-main instance and "Check if the shape is inside a component non-main instance and
is not the root shape." is not the root shape."
[shape] [shape]

View file

@ -8,7 +8,9 @@
(:require (:require
[app.common.data :as d] [app.common.data :as d]
[app.common.data.macros :as dm] [app.common.data.macros :as dm]
[app.common.files.features :as feat])) [app.common.files.features :as feat]
[app.common.time :as dt]
[app.common.types.component :as ctk]))
(defn components (defn components
[file-data] [file-data]
@ -23,6 +25,10 @@
[file-data] [file-data]
(filter :deleted (vals (:components file-data)))) (filter :deleted (vals (:components file-data))))
(defn- touch
[component]
(assoc component :modified-at (dt/now)))
(defn add-component (defn add-component
[file-data {:keys [id name path main-instance-id main-instance-page shapes]}] [file-data {:keys [id name path main-instance-id main-instance-page shapes]}]
(let [components-v2 (dm/get-in file-data [:options :components-v2]) (let [components-v2 (dm/get-in file-data [:options :components-v2])
@ -30,9 +36,9 @@
(cond-> file-data (cond-> file-data
:always :always
(assoc-in [:components id] (assoc-in [:components id]
{:id id (touch {:id id
:name name :name name
:path path}) :path path}))
(not components-v2) (not components-v2)
(assoc-in [:components id :objects] (assoc-in [:components id :objects]
@ -47,24 +53,27 @@
(defn mod-component (defn mod-component
[file-data {:keys [id name path objects annotation]}] [file-data {:keys [id name path objects annotation]}]
(let [wrap-objects-fn feat/*wrap-with-objects-map-fn*] (let [wrap-objects-fn feat/*wrap-with-objects-map-fn*]
(update-in file-data [:components id] (d/update-in-when file-data [:components id]
(fn [component] (fn [component]
(let [objects (some-> objects wrap-objects-fn)] (let [objects (some-> objects wrap-objects-fn)]
(cond-> component (cond-> component
(some? name) (some? name)
(assoc :name name) (assoc :name name)
(some? path) (some? path)
(assoc :path path) (assoc :path path)
(some? objects) (some? objects)
(assoc :objects objects) (assoc :objects objects)
(some? annotation) (some? annotation)
(assoc :annotation annotation) (assoc :annotation annotation)
(nil? annotation) (nil? annotation)
(dissoc :annotation))))))) (dissoc :annotation)
:always
(touch)))))))
(defn get-component (defn get-component
([file-data component-id] ([file-data component-id]
@ -83,8 +92,13 @@
component))) component)))
(defn update-component (defn update-component
[file-data component-id f] [file-data component-id f & args]
(update-in file-data [:components component-id] f)) (d/update-in-when file-data [:components component-id] #(-> (apply f % args)
(touch))))
(defn set-component-modified
[file-data component-id]
(update-component file-data component-id identity))
(defn delete-component (defn delete-component
[file-data component-id] [file-data component-id]
@ -92,8 +106,19 @@
(defn mark-component-deleted (defn mark-component-deleted
[file-data component-id] [file-data component-id]
(assoc-in file-data [:components component-id :deleted] true)) (d/update-in-when file-data [:components component-id] assoc :deleted true))
(defn mark-component-undeleted (defn mark-component-undeleted
[file-data component-id] [file-data component-id]
(d/dissoc-in file-data [:components component-id :deleted])) (d/dissoc-in file-data [:components component-id :deleted]))
(defn used-components-changed-since
"Check if the shape is an instance of any component in the library, and
the component has been modified after the date."
[shape library since-date]
(if (ctk/uses-library-components? shape (:id library))
(let [component (get-component (:data library) (:component-id shape))]
(if (< (:modified-at component) since-date) ;; Note that :modified-at may be nil
[]
[[(:id shape) (:component-id shape) :component]]))
[]))

View file

@ -10,6 +10,7 @@
[app.common.geom.shapes :as gsh] [app.common.geom.shapes :as gsh]
[app.common.pages.common :as common] [app.common.pages.common :as common]
[app.common.spec :as us] [app.common.spec :as us]
[app.common.types.component :as ctk]
[app.common.types.components-list :as ctkl] [app.common.types.components-list :as ctkl]
[app.common.types.pages-list :as ctpl] [app.common.types.pages-list :as ctpl]
[app.common.types.shape-tree :as ctst] [app.common.types.shape-tree :as ctst]
@ -20,10 +21,11 @@
(s/def ::id uuid?) (s/def ::id uuid?)
(s/def ::name ::us/string) (s/def ::name ::us/string)
(s/def ::path (s/nilable ::us/string)) (s/def ::path (s/nilable ::us/string))
(s/def ::modified-at ::us/inst)
(s/def ::container (s/def ::container
(s/keys :req-un [::id ::name] (s/keys :req-un [::id ::name]
:opt-un [::type ::path ::ctst/objects])) :opt-un [::type ::path ::modified-at ::ctst/objects]))
(defn make-container (defn make-container
[page-or-component type] [page-or-component type]
@ -70,14 +72,20 @@
(defn get-component-shape (defn get-component-shape
"Get the parent shape linked to a component for this shape, if any" "Get the parent shape linked to a component for this shape, if any"
[objects shape] ([objects shape] (get-component-shape objects shape nil))
(if-not (:shape-ref shape) ([objects shape {:keys [allow-main?] :or {allow-main? false} :as options}]
(cond
(nil? shape)
nil nil
(if (:component-id shape)
shape (and (not (ctk/in-component-copy? shape)) (not allow-main?))
(if-let [parent-id (:parent-id shape)] nil
(get-component-shape objects (get objects parent-id))
nil)))) (ctk/instance-root? shape)
shape
:else
(get-component-shape objects (get objects (:parent-id shape)) options))))
(defn make-component-shape (defn make-component-shape
"Clone the shape and all children. Generate new ids and detach "Clone the shape and all children. Generate new ids and detach

View file

@ -281,6 +281,22 @@
(some used-in-container? (containers-seq file-data)))) (some used-in-container? (containers-seq file-data))))
(defn used-assets-changed-since
"Get a lazy sequence of all assets in the library that are in use by the file and have
been modified after the given date."
[file-data library since-date]
(letfn [(used-assets-shape [shape]
(concat
(ctkl/used-components-changed-since shape library since-date)
(ctcl/used-colors-changed-since shape library since-date)
(ctyl/used-typographies-changed-since shape library since-date)))
(used-assets-container [container]
(->> (mapcat used-assets-shape (ctn/shapes-seq container))
(map #(cons (:id container) %))))]
(mapcat used-assets-container (containers-seq file-data))))
(defn get-or-add-library-page (defn get-or-add-library-page
"If exists a page named 'Library backup', get the id and calculate the position to start "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)." adding new components. If not, create it and start at (0, 0)."
@ -370,7 +386,7 @@
root-to-board root-to-board
(fn [shape] (fn [shape]
(cond-> shape (cond-> shape
(and (ctk/instance-root? shape) (and (ctk/instance-head? shape)
(not= (:type shape) :frame)) (not= (:type shape) :frame))
(assoc :type :frame (assoc :type :frame
:fills [] :fills []
@ -548,75 +564,81 @@
(defn dump-tree (defn dump-tree
([file-data page-id libraries] ([file-data page-id libraries]
(dump-tree file-data page-id libraries false false)) (dump-tree file-data page-id libraries false false false))
([file-data page-id libraries show-ids] ([file-data page-id libraries show-ids]
(dump-tree file-data page-id libraries show-ids false)) (dump-tree file-data page-id libraries show-ids false false))
([file-data page-id libraries show-ids show-touched] ([file-data page-id libraries show-ids show-touched]
(dump-tree file-data page-id libraries show-ids show-touched false))
([file-data page-id libraries show-ids show-touched show-modified]
(let [page (ctpl/get-page file-data page-id) (let [page (ctpl/get-page file-data page-id)
objects (:objects page) objects (:objects page)
components (ctkl/components file-data) components (ctkl/components file-data)
root (d/seek #(nil? (:parent-id %)) (vals objects))] root (d/seek #(nil? (:parent-id %)) (vals objects))]
(letfn [(show-shape [shape-id level objects] (letfn [(show-shape [shape-id level objects]
(let [shape (get objects shape-id)] (let [shape (get objects shape-id)]
(println (str/pad (str (str/repeat " " level) (println (str/pad (str (str/repeat " " level)
(when (:main-instance? shape) "{") (when (:main-instance? shape) "{")
(:name shape) (:name shape)
(when (:main-instance? shape) "}") (when (:main-instance? shape) "}")
(when (seq (:touched shape)) "*") (when (seq (:touched shape)) "*")
(when show-ids (str/format " <%s>" (:id shape)))) (when show-ids (str/format " <%s>" (:id shape))))
{:length 20 {:length 20
:type :right}) :type :right})
(show-component-info shape objects)) (show-component-info shape objects))
(when show-touched (when show-touched
(when (seq (:touched shape)) (when (seq (:touched shape))
(println (str (str/repeat " " level) (println (str (str/repeat " " level)
" " " "
(str (:touched shape))))) (str (:touched shape)))))
(when (:remote-synced? shape) (when (:remote-synced? shape)
(println (str (str/repeat " " level) (println (str (str/repeat " " level)
" (remote-synced)")))) " (remote-synced)"))))
(when (:shapes shape) (when (:shapes shape)
(dorun (for [shape-id (:shapes shape)] (dorun (for [shape-id (:shapes shape)]
(show-shape shape-id (inc level) objects)))))) (show-shape shape-id (inc level) objects))))))
(show-component-info [shape objects] (show-component-info [shape objects]
(if (nil? (:shape-ref shape)) (if (nil? (:shape-ref shape))
(if (:component-root? shape) " #" "") (if (:component-root? shape) " #" "")
(let [root-shape (ctn/get-component-shape objects shape) (let [root-shape (ctn/get-component-shape objects shape)
component-id (when root-shape (:component-id root-shape)) component-id (when root-shape (:component-id root-shape))
component-file-id (when root-shape (:component-file 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-file (when component-file-id (get libraries component-file-id nil))
component (when component-id component (when component-id
(if component-file (if component-file
(ctkl/get-component (:data component-file) component-id) (ctkl/get-component (:data component-file) component-id)
(get components component-id))) (get components component-id)))
component-shape (when component component-shape (when component
(if component-file (if component-file
(get-ref-shape (:data component-file) component shape) (get-ref-shape (:data component-file) component shape)
(get-ref-shape file-data component shape)))] (get-ref-shape file-data component shape)))]
(str/format " %s--> %s%s%s" (str/format " %s--> %s%s%s"
(cond (:component-root? shape) "#" (cond (:component-root? shape) "#"
(:component-id shape) "@" (:component-id shape) "@"
:else "-") :else "-")
(when component-file (str/format "<%s> " (:name component-file)))
(or (:name component-shape) "?") (when component-file (str/format "<%s> " (:name component-file)))
(if (or (:component-root? shape)
(nil? (:component-id shape)) (or (:name component-shape) "?")
true)
"" (if (or (:component-root? shape)
(let [component-id (:component-id shape) (nil? (:component-id shape))
component-file-id (:component-file shape) true)
component-file (when component-file-id (get libraries component-file-id nil)) ""
component (if component-file (let [component-id (:component-id shape)
(ctkl/get-component (:data component-file) component-id) component-file-id (:component-file shape)
(get components component-id))] component-file (when component-file-id (get libraries component-file-id nil))
(str/format " (%s%s)" component (if component-file
(when component-file (str/format "<%s> " (:name component-file))) (ctkl/get-component (:data component-file) component-id)
(:name component)))))))) (get components component-id))]
(str/format " (%s%s)"
(when component-file (str/format "<%s> " (:name component-file)))
(:name component))))))))
(show-component-instance [component] (show-component-instance [component]
(let [page (get-component-page file-data component) (let [page (get-component-page file-data component)
@ -633,7 +655,10 @@
(dorun (for [component (vals components)] (dorun (for [component (vals components)]
(do (do
(println) (println)
(println (str/format "[%s]" (:name component))) (println (str/format "[%s]%s%s"
(:name component)
(when show-ids (str " " (:id component)))
(when show-modified (str " " (:modified-at component)))))
(when (:objects component) (when (:objects component)
(show-shape (:id component) 0 (:objects component))) (show-shape (:id component) 0 (:objects component)))
(when (:main-instance-page component) (when (:main-instance-page component)

View file

@ -118,8 +118,8 @@
(filter cph/frame-shape?))] (filter cph/frame-shape?))]
(->> (keys objects) (->> (keys objects)
(into [] xform)))) (into [] xform))))
(remove #(or (and skip-components? (ctk/instance-root? %)) (remove #(or (and skip-components? (ctk/instance-head? %))
(and skip-copies? (and (ctk/instance-root? %) (not (ctk/main-instance? %))))))))) (and skip-copies? (and (ctk/instance-head? %) (not (ctk/main-instance? %)))))))))
(defn get-frames-ids (defn get-frames-ids
"Retrieves all frame ids as vector" "Retrieves all frame ids as vector"

View file

@ -4,25 +4,53 @@
;; ;;
;; Copyright (c) KALEIDOS INC ;; Copyright (c) KALEIDOS INC
(ns app.common.types.typographies-list) (ns app.common.types.typographies-list
(:require
[app.common.data :as d]
[app.common.text :as txt]
[app.common.time :as dt]))
(defn typographies-seq (defn typographies-seq
[file-data] [file-data]
(vals (:typographies file-data))) (vals (:typographies file-data)))
(defn- touch
[typography]
(assoc typography :modified-at (dt/now)))
(defn add-typography (defn add-typography
[file-data typography] [file-data typography]
(update file-data :typographies assoc (:id typography) typography)) (update file-data :typographies assoc (:id typography) (touch typography)))
(defn get-typography (defn get-typography
[file-data typography-id] [file-data typography-id]
(get-in file-data [:typographies typography-id])) (get-in file-data [:typographies typography-id]))
(defn get-ref-typography
[library-data typography]
(when (= (:typography-ref-file typography) (:id library-data))
(get-typography library-data (:typography-ref-id typography))))
(defn set-typography
[file-data typography]
(d/assoc-in-when file-data [:typographies (:id typography)] (touch typography)))
(defn update-typography (defn update-typography
[file-data typography-id f] [file-data typography-id f & args]
(update-in file-data [:typographies typography-id] f)) (d/update-in-when file-data [:typographies typography-id] #(-> (apply f % args)
(touch))))
(defn delete-typography (defn delete-typography
[file-data typography-id] [file-data typography-id]
(update file-data :typographies dissoc typography-id)) (update file-data :typographies dissoc typography-id))
(defn used-typographies-changed-since
"Find all usages of any typography in the library by the given shape, of
typographies that have ben modified after the date.."
[shape library since-date]
(->> shape
:content
txt/node-seq
(keep #(get-ref-typography (:data library) %))
(remove #(< (:modified-at %) since-date)) ;; Note that :modified-at may be nil
(map #(vector (:id shape) (:id %) :typography))))

View file

@ -6,6 +6,7 @@
(ns app.common.types.typography (ns app.common.types.typography
(:require (:require
[app.common.spec :as us]
[app.common.text :as txt] [app.common.text :as txt]
[clojure.spec.alpha :as s])) [clojure.spec.alpha :as s]))
@ -21,6 +22,7 @@
(s/def ::line-height string?) (s/def ::line-height string?)
(s/def ::letter-spacing string?) (s/def ::letter-spacing string?)
(s/def ::text-transform string?) (s/def ::text-transform string?)
(s/def ::modified-at ::us/inst)
(s/def ::typography (s/def ::typography
(s/keys :req-un [::id (s/keys :req-un [::id
@ -34,7 +36,8 @@
::line-height ::line-height
::letter-spacing ::letter-spacing
::text-transform] ::text-transform]
:opt-un [::path])) :opt-un [::path
::modified-at]))
(defn uses-library-typographies? (defn uses-library-typographies?
"Check if the shape uses any typography in the given library." "Check if the shape uses any typography in the given library."

View file

@ -305,10 +305,10 @@ isexe@^2.0.0:
resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10"
integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA= integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=
luxon@^3.1.1: luxon@^3.3.0:
version "3.1.1" version "3.3.0"
resolved "https://registry.yarnpkg.com/luxon/-/luxon-3.1.1.tgz#b492c645b2474fb86f3bd3283213846b99c32c1e" resolved "https://registry.yarnpkg.com/luxon/-/luxon-3.3.0.tgz#d73ab5b5d2b49a461c47cedbc7e73309b4805b48"
integrity sha512-Ah6DloGmvseB/pX1cAmjbFvyU/pKuwQMQqz7d0yvuDlVYLTs2WeDHQMpC8tGjm1da+BriHROW/OEIT/KfYg6xw== integrity sha512-An0UCfG/rSiqtAIiBPO0Y9/zAnHUZxAMiCpTd5h2smgsj7GGmcenvrvww2cqNA8/4A5ZrD1gJpHN2mIHZQF+Mg==
md5.js@^1.3.4: md5.js@^1.3.4:
version "1.3.5" version "1.3.5"

View file

@ -59,7 +59,7 @@
"highlight.js": "^11.7.0", "highlight.js": "^11.7.0",
"js-beautify": "^1.14.7", "js-beautify": "^1.14.7",
"jszip": "^3.10.1", "jszip": "^3.10.1",
"luxon": "^3.1.1", "luxon": "^3.3.0",
"mousetrap": "^1.6.5", "mousetrap": "^1.6.5",
"opentype.js": "^1.3.4", "opentype.js": "^1.3.4",
"postcss-modules": "^6.0.0", "postcss-modules": "^6.0.0",

View file

@ -260,12 +260,11 @@
ptk/WatchEvent ptk/WatchEvent
(watch [_ state _] (watch [_ state _]
(let [ignore-until (-> state :workspace-file :ignore-sync-until) (let [file-data (:workspace-data state)
file-id (-> state :workspace-file :id) ignore-until (dm/get-in state [:workspace-file :ignore-sync-until])
needs-update? (some #(and (> (:modified-at %) (:synced-at %)) file-id (dm/get-in state [:workspace-file :id])
(or (not ignore-until) needs-update? (seq (filter #(dwl/assets-need-sync % file-data ignore-until)
(> (:modified-at %) ignore-until))) libraries))]
libraries)]
(when needs-update? (when needs-update?
(rx/of (dwl/notify-sync-file file-id))))))) (rx/of (dwl/notify-sync-file file-id)))))))
@ -1633,7 +1632,7 @@
;; Check if the shape is an instance whose master is defined in a ;; Check if the shape is an instance whose master is defined in a
;; library that is not linked to the current file ;; library that is not linked to the current file
(foreign-instance? [shape paste-objects state] (foreign-instance? [shape paste-objects state]
(let [root (cph/get-root-shape paste-objects shape) (let [root (ctn/get-component-shape paste-objects shape)
root-file-id (:component-file root)] root-file-id (:component-file root)]
(and (some? root) (and (some? root)
(not= root-file-id (:current-file-id state)) (not= root-file-id (:current-file-id state))

View file

@ -91,7 +91,7 @@
;; Shapes that are in a component, but are not root, must be detached, ;; Shapes that are in a component, but are not root, must be detached,
;; because they will be now children of a non instance group. ;; because they will be now children of a non instance group.
shapes-to-detach (filter ctk/in-component-instance-not-root? shapes) shapes-to-detach (filter ctk/in-component-copy-not-root? shapes)
;; Look at the `get-empty-groups-after-group-creation` ;; Look at the `get-empty-groups-after-group-creation`
;; docstring to understand the real purpose of this code ;; docstring to understand the real purpose of this code
@ -124,7 +124,7 @@
;; Shapes that are in a component (including root) must be detached, ;; Shapes that are in a component (including root) must be detached,
;; because cannot be easyly synchronized back to the main component. ;; because cannot be easyly synchronized back to the main component.
shapes-to-detach (filter ctk/in-component-instance? shapes-to-detach (filter ctk/in-component-copy?
(cph/get-children-with-self objects (:id group)))] (cph/get-children-with-self objects (:id group)))]
(-> (pcb/empty-changes it page-id) (-> (pcb/empty-changes it page-id)

View file

@ -605,7 +605,7 @@
container (cph/get-container local-file :page page-id) container (cph/get-container local-file :page page-id)
shape (ctn/get-shape container id)] shape (ctn/get-shape container id)]
(when (ctk/in-component-instance? shape) (when (ctk/instance-head? shape)
(let [libraries (wsh/get-libraries state) (let [libraries (wsh/get-libraries state)
changes changes
@ -801,6 +801,8 @@
(rx/of (dch/commit-changes (assoc changes :file-id file-id)))))))) (rx/of (dch/commit-changes (assoc changes :file-id file-id))))))))
(def ignore-sync (def ignore-sync
"Mark the file as ignore syncs. All library changes before this moment will not
ber notified to sync."
(ptk/reify ::ignore-sync (ptk/reify ::ignore-sync
ptk/UpdateEvent ptk/UpdateEvent
(update [_ state] (update [_ state]
@ -812,13 +814,26 @@
{:file-id (get-in state [:workspace-file :id]) {:file-id (get-in state [:workspace-file :id])
:date (dt/now)})))) :date (dt/now)}))))
(defn assets-need-sync
"Get a lazy sequence of all the assets of each type in the library that have
been modified after the last sync of the library. The sync date may be
overriden by providing a ignore-until parameter.
The sequence items are tuples of (page-id shape-id asset-id asset-type)."
([library file-data] (assets-need-sync library file-data nil))
([library file-data ignore-until]
(let [sync-date (max (:synced-at library) (or ignore-until 0))]
(when (> (:modified-at library) sync-date)
(ctf/used-assets-changed-since file-data library sync-date)))))
(defn notify-sync-file (defn notify-sync-file
[file-id] [file-id]
(us/assert ::us/uuid file-id) (us/assert ::us/uuid file-id)
(ptk/reify ::notify-sync-file (ptk/reify ::notify-sync-file
ptk/WatchEvent ptk/WatchEvent
(watch [_ state _] (watch [_ state _]
(let [libraries-need-sync (filter #(> (:modified-at %) (:synced-at %)) (let [file-data (:workspace-data state)
libraries-need-sync (filter #(seq (assets-need-sync % file-data))
(vals (get state :workspace-libraries))) (vals (get state :workspace-libraries)))
do-update #(do (apply st/emit! (map (fn [library] do-update #(do (apply st/emit! (map (fn [library]
(sync-file (:current-file-id state) (sync-file (:current-file-id state)
@ -828,14 +843,15 @@
do-dismiss #(do (st/emit! ignore-sync) do-dismiss #(do (st/emit! ignore-sync)
(st/emit! dm/hide))] (st/emit! dm/hide))]
(rx/of (dm/info-dialog (when (seq libraries-need-sync)
(tr "workspace.updates.there-are-updates") (rx/of (dm/info-dialog
:inline-actions (tr "workspace.updates.there-are-updates")
[{:label (tr "workspace.updates.update") :inline-actions
:callback do-update} [{:label (tr "workspace.updates.update")
{:label (tr "workspace.updates.dismiss") :callback do-update}
:callback do-dismiss}] {:label (tr "workspace.updates.dismiss")
:sync-dialog)))))) :callback do-dismiss}]
:sync-dialog)))))))
(defn watch-component-changes (defn watch-component-changes
"Watch the state for changes that affect to any main instance. If a change is detected will throw "Watch the state for changes that affect to any main instance. If a change is detected will throw

View file

@ -99,7 +99,7 @@
(if (and (= (count shapes) 1) (if (and (= (count shapes) 1)
(or (and (= (:type (first shapes)) :group) (not components-v2)) (or (and (= (:type (first shapes)) :group) (not components-v2))
(= (:type (first shapes)) :frame)) (= (:type (first shapes)) :frame))
(not (ctk/instance-root? (first shapes)))) (not (ctk/instance-head? (first shapes))))
[(first shapes) (-> (pcb/empty-changes it page-id) [(first shapes) (-> (pcb/empty-changes it page-id)
(pcb/with-objects objects))] (pcb/with-objects objects))]
(let [root-name (if (= 1 (count shapes)) (let [root-name (if (= 1 (count shapes))
@ -111,7 +111,7 @@
page-id page-id
shapes shapes
root-name root-name
(not (ctk/instance-root? (first shapes)))) (not (ctk/instance-head? (first shapes))))
(prepare-create-board changes (prepare-create-board changes
(uuid/next) (uuid/next)
(:parent-id (first shapes)) (:parent-id (first shapes))
@ -203,7 +203,7 @@
(defn- generate-detach-recursive (defn- generate-detach-recursive
[changes container shape-id first] [changes container shape-id first]
(let [shape (ctn/get-shape container shape-id)] (let [shape (ctn/get-shape container shape-id)]
(if (and (ctk/instance-root? shape) (not first)) (if (and (ctk/instance-head? shape) (not first))
;; Subinstances are not detached, but converted in top instances ;; Subinstances are not detached, but converted in top instances
(pcb/update-shapes changes [(:id shape)] #(assoc % :component-root? true)) (pcb/update-shapes changes [(:id shape)] #(assoc % :component-root? true))
;; Otherwise, detach the shape and all children ;; Otherwise, detach the shape and all children
@ -530,7 +530,7 @@
[changes libraries container shape-id reset? components-v2] [changes libraries container shape-id reset? components-v2]
(log/debug :msg "Sync shape direct" :shape (str shape-id) :reset? reset?) (log/debug :msg "Sync shape direct" :shape (str shape-id) :reset? reset?)
(let [shape-inst (ctn/get-shape container shape-id)] (let [shape-inst (ctn/get-shape container shape-id)]
(if (ctk/in-component-instance? shape-inst) (if (ctk/in-component-copy? shape-inst)
(let [library (dm/get-in libraries [(:component-file shape-inst) :data]) (let [library (dm/get-in libraries [(:component-file shape-inst) :data])
component (or (ctkl/get-component library (:component-id shape-inst)) component (or (ctkl/get-component library (:component-id shape-inst))
(and reset? (and reset?

View file

@ -15,6 +15,7 @@
[app.common.pages.common :as cpc] [app.common.pages.common :as cpc]
[app.common.pages.helpers :as cph] [app.common.pages.helpers :as cph]
[app.common.spec :as us] [app.common.spec :as us]
[app.common.types.container :as ctn]
[app.common.types.modifiers :as ctm] [app.common.types.modifiers :as ctm]
[app.common.types.shape.layout :as ctl] [app.common.types.shape.layout :as ctl]
[app.main.constants :refer [zoom-half-pixel-precision]] [app.main.constants :refer [zoom-half-pixel-precision]]
@ -54,7 +55,7 @@
shape shape
(nil? root) (nil? root)
(cph/get-root-shape objects shape) (ctn/get-component-shape objects shape {:allow-main? true})
:else root) :else root)
@ -64,7 +65,7 @@
transformed-shape transformed-shape
(nil? transformed-root) (nil? transformed-root)
(as-> (cph/get-root-shape objects transformed-shape) $ (as-> (ctn/get-component-shape objects transformed-shape {:allow-main? true}) $
(gsh/transform-shape (merge $ (get modif-tree (:id $))))) (gsh/transform-shape (merge $ (get modif-tree (:id $)))))
:else transformed-root) :else transformed-root)

View file

@ -189,7 +189,7 @@
;; but hidden (to be able to recover them more easily). ;; but hidden (to be able to recover them more easily).
(let [shape (get objects shape-id) (let [shape (get objects shape-id)
component-shape (ctn/get-component-shape objects shape)] component-shape (ctn/get-component-shape objects shape)]
(and (ctk/in-component-instance? shape) (and (ctk/in-component-copy? shape)
(not= shape component-shape) (not= shape component-shape)
(not (ctk/main-instance? component-shape))))) (not (ctk/main-instance? component-shape)))))

View file

@ -14,7 +14,7 @@
(mf/defc element-icon (mf/defc element-icon
[{:keys [shape main-instance?] :as props}] [{:keys [shape main-instance?] :as props}]
(if (ctk/instance-root? shape) (if (ctk/instance-head? shape)
(if main-instance? (if main-instance?
i/component i/component
i/component-copy) i/component-copy)

View file

@ -14,7 +14,7 @@
(mf/defc element-icon-refactor (mf/defc element-icon-refactor
[{:keys [shape main-instance?] :as props}] [{:keys [shape main-instance?] :as props}]
(if (ctk/instance-root? shape) (if (ctk/instance-head? shape)
(if main-instance? (if main-instance?
i/component-refactor i/component-refactor
i/copy-refactor) i/copy-refactor)

View file

@ -438,7 +438,7 @@
has-component? (some true? (map #(contains? % :component-id) shapes)) has-component? (some true? (map #(contains? % :component-id) shapes))
is-component? (and single? (-> shapes first :component-id some?)) is-component? (and single? (-> shapes first :component-id some?))
is-non-root? (and single? (ctk/in-component-instance-not-root? (first shapes))) is-non-root? (and single? (ctk/in-component-copy-not-root? (first shapes)))
first-shape (first shapes) first-shape (first shapes)
{:keys [shape-id component-id component-file main-instance?]} first-shape {:keys [shape-id component-id component-file main-instance?]} first-shape

View file

@ -224,7 +224,7 @@
#(as-> (get objects %) obj #(as-> (get objects %) obj
(and (cph/root-frame? obj) (and (cph/root-frame? obj)
(d/not-empty? (:shapes obj)) (d/not-empty? (:shapes obj))
(not (ctk/instance-root? obj)) (not (ctk/instance-head? obj))
(not (ctk/main-instance? obj)))) (not (ctk/main-instance? obj))))
;; Set with the elements to remove from the hover list ;; Set with the elements to remove from the hover list

View file

@ -22,12 +22,13 @@
["date-fns/locale/ru" :default dateFnsLocalesRu] ["date-fns/locale/ru" :default dateFnsLocalesRu]
["date-fns/locale/tr" :default dateFnsLocalesTr] ["date-fns/locale/tr" :default dateFnsLocalesTr]
["date-fns/locale/zh-CN" :default dateFnsLocalesZhCn] ["date-fns/locale/zh-CN" :default dateFnsLocalesZhCn]
["luxon" :as lxn] [app.common.data.macros :as dm]
[app.common.time :as common-time]
[app.util.object :as obj] [app.util.object :as obj]
[cuerdas.core :as str])) [cuerdas.core :as str]))
(def DateTime lxn/DateTime) (dm/export common-time/DateTime)
(def Duration lxn/Duration) (dm/export common-time/Duration)
(defprotocol ITimeMath (defprotocol ITimeMath
(plus [_ o]) (plus [_ o])
@ -89,9 +90,7 @@
:rfc2822 (.fromRFC2822 ^js DateTime s #js {:zone zone :setZone force-zone}) :rfc2822 (.fromRFC2822 ^js DateTime s #js {:zone zone :setZone force-zone})
:http (.fromHTTP ^js DateTime s #js {:zone zone :setZone force-zone}))))) :http (.fromHTTP ^js DateTime s #js {:zone zone :setZone force-zone})))))
(defn now (dm/export common-time/now)
[]
(.local ^js DateTime))
(defn utc-now (defn utc-now
[] []

View file

@ -295,18 +295,20 @@
nil)) nil))
(defn dump-tree' (defn dump-tree'
([state] (dump-tree' state false false)) ([state] (dump-tree' state false false false))
([state show-ids] (dump-tree' state show-ids false)) ([state show-ids] (dump-tree' state show-ids false false))
([state show-ids show-touched] ([state show-ids show-touched] (dump-tree' state show-ids show-touched false))
([state show-ids show-touched show-modified]
(let [page-id (get state :current-page-id) (let [page-id (get state :current-page-id)
file-data (get state :workspace-data) file-data (get state :workspace-data)
libraries (get state :workspace-libraries)] libraries (get state :workspace-libraries)]
(ctf/dump-tree file-data page-id libraries show-ids show-touched)))) (ctf/dump-tree file-data page-id libraries show-ids show-touched show-modified))))
(defn ^:export dump-tree (defn ^:export dump-tree
([] (dump-tree' @st/state)) ([] (dump-tree' @st/state))
([show-ids] (dump-tree' @st/state show-ids)) ([show-ids] (dump-tree' @st/state show-ids false false))
([show-ids show-touched] (dump-tree' @st/state show-ids show-touched))) ([show-ids show-touched] (dump-tree' @st/state show-ids show-touched false))
([show-ids show-touched show-modified] (dump-tree' @st/state show-ids show-touched show-modified)))
(when *assert* (when *assert*
(defonce debug-subscription (defonce debug-subscription

View file

@ -3342,10 +3342,10 @@ lru-queue@^0.1.0:
dependencies: dependencies:
es5-ext "~0.10.2" es5-ext "~0.10.2"
luxon@^3.1.1: luxon@^3.3.0:
version "3.1.1" version "3.3.0"
resolved "https://registry.yarnpkg.com/luxon/-/luxon-3.1.1.tgz#b492c645b2474fb86f3bd3283213846b99c32c1e" resolved "https://registry.yarnpkg.com/luxon/-/luxon-3.3.0.tgz#d73ab5b5d2b49a461c47cedbc7e73309b4805b48"
integrity sha512-Ah6DloGmvseB/pX1cAmjbFvyU/pKuwQMQqz7d0yvuDlVYLTs2WeDHQMpC8tGjm1da+BriHROW/OEIT/KfYg6xw== integrity sha512-An0UCfG/rSiqtAIiBPO0Y9/zAnHUZxAMiCpTd5h2smgsj7GGmcenvrvww2cqNA8/4A5ZrD1gJpHN2mIHZQF+Mg==
make-iterator@^1.0.0: make-iterator@^1.0.0:
version "1.0.1" version "1.0.1"